Comprehensive Testing Guide

Testing is crucial for building reliable StateSet integrations. This guide covers testing strategies from unit tests to end-to-end scenarios, helping you build confidence in your integration.

Testing Overview

Unit Testing

Test individual functions and components in isolation

Integration Testing

Test interactions between your code and StateSet APIs

End-to-End Testing

Test complete workflows from start to finish

Test Environment Setup

Testing Pyramid Strategy

Environment Configuration

// jest.config.js
module.exports = {
  testEnvironment: 'node',
  setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
  testMatch: [
    '**/__tests__/**/*.test.js',
    '**/*.test.js'
  ],
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/**/*.test.js',
    '!src/index.js'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  testTimeout: 30000
};

// tests/setup.js
import dotenv from 'dotenv';
import { jest } from '@jest/globals';

// Load test environment variables
dotenv.config({ path: '.env.test' });

// Global test setup
beforeAll(() => {
  // Mock console methods in tests
  global.console = {
    ...console,
    log: jest.fn(),
    error: jest.fn(),
    warn: jest.fn(),
  };
});

afterAll(() => {
  // Cleanup after all tests
  jest.restoreAllMocks();
});

Unit Testing

Testing Individual Functions

// src/orderProcessor.js
import { StateSetClient } from 'stateset-node';
import winston from 'winston';

export class OrderProcessor {
  constructor(client, logger = winston.createLogger()) {
    this.client = client;
    this.logger = logger;
  }

  async processOrder(orderData) {
    try {
      // Validate order data
      const validation = this.validateOrder(orderData);
      if (!validation.isValid) {
        throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
      }

      // Create order
      const order = await this.client.orders.create(orderData);
      
      this.logger.info('Order processed successfully', { orderId: order.id });
      return order;
    } catch (error) {
      this.logger.error('Order processing failed', { error: error.message });
      throw error;
    }
  }

  validateOrder(orderData) {
    const errors = [];
    
    if (!orderData.customer_email) {
      errors.push('Customer email is required');
    }
    
    if (!orderData.items || orderData.items.length === 0) {
      errors.push('Order must contain at least one item');
    }
    
    if (orderData.items) {
      orderData.items.forEach((item, index) => {
        if (!item.sku) errors.push(`Item ${index}: SKU is required`);
        if (!item.quantity || item.quantity <= 0) {
          errors.push(`Item ${index}: Quantity must be positive`);
        }
      });
    }
    
    return {
      isValid: errors.length === 0,
      errors
    };
  }
}

// __tests__/orderProcessor.test.js
import { OrderProcessor } from '../src/orderProcessor.js';
import { jest } from '@jest/globals';

describe('OrderProcessor', () => {
  let orderProcessor;
  let mockClient;
  let mockLogger;

  beforeEach(() => {
    mockClient = {
      orders: {
        create: jest.fn()
      }
    };
    
    mockLogger = {
      info: jest.fn(),
      error: jest.fn()
    };
    
    orderProcessor = new OrderProcessor(mockClient, mockLogger);
  });

  describe('validateOrder', () => {
    it('should return valid for correct order data', () => {
      const orderData = {
        customer_email: 'test@example.com',
        items: [
          { sku: 'ITEM-001', quantity: 2 }
        ]
      };

      const result = orderProcessor.validateOrder(orderData);
      
      expect(result.isValid).toBe(true);
      expect(result.errors).toHaveLength(0);
    });

    it('should return invalid for missing email', () => {
      const orderData = {
        items: [{ sku: 'ITEM-001', quantity: 2 }]
      };

      const result = orderProcessor.validateOrder(orderData);
      
      expect(result.isValid).toBe(false);
      expect(result.errors).toContain('Customer email is required');
    });

    it('should return invalid for empty items', () => {
      const orderData = {
        customer_email: 'test@example.com',
        items: []
      };

      const result = orderProcessor.validateOrder(orderData);
      
      expect(result.isValid).toBe(false);
      expect(result.errors).toContain('Order must contain at least one item');
    });

    it('should validate individual items', () => {
      const orderData = {
        customer_email: 'test@example.com',
        items: [
          { sku: 'ITEM-001', quantity: 2 },
          { quantity: 1 }, // Missing SKU
          { sku: 'ITEM-003', quantity: -1 } // Invalid quantity
        ]
      };

      const result = orderProcessor.validateOrder(orderData);
      
      expect(result.isValid).toBe(false);
      expect(result.errors).toContain('Item 1: SKU is required');
      expect(result.errors).toContain('Item 2: Quantity must be positive');
    });
  });

  describe('processOrder', () => {
    it('should create order successfully', async () => {
      const orderData = {
        customer_email: 'test@example.com',
        items: [{ sku: 'ITEM-001', quantity: 2 }]
      };
      
      const expectedOrder = { id: 'order_123', ...orderData };
      mockClient.orders.create.mockResolvedValue(expectedOrder);

      const result = await orderProcessor.processOrder(orderData);

      expect(mockClient.orders.create).toHaveBeenCalledWith(orderData);
      expect(mockLogger.info).toHaveBeenCalledWith(
        'Order processed successfully',
        { orderId: 'order_123' }
      );
      expect(result).toEqual(expectedOrder);
    });

    it('should throw error for invalid order data', async () => {
      const orderData = { items: [] }; // Invalid data

      await expect(orderProcessor.processOrder(orderData)).rejects.toThrow(
        'Validation failed'
      );
      
      expect(mockClient.orders.create).not.toHaveBeenCalled();
      expect(mockLogger.error).toHaveBeenCalled();
    });

    it('should handle API errors', async () => {
      const orderData = {
        customer_email: 'test@example.com',
        items: [{ sku: 'ITEM-001', quantity: 2 }]
      };
      
      const apiError = new Error('API Error');
      mockClient.orders.create.mockRejectedValue(apiError);

      await expect(orderProcessor.processOrder(orderData)).rejects.toThrow('API Error');
      
      expect(mockLogger.error).toHaveBeenCalledWith(
        'Order processing failed',
        { error: 'API Error' }
      );
    });
  });
});

Integration Testing

Testing API Interactions

// __tests__/integration/orders.integration.test.js
import { StateSetClient } from 'stateset-node';
import { OrderProcessor } from '../../src/orderProcessor.js';

describe('Orders Integration Tests', () => {
  let client;
  let orderProcessor;
  let createdOrderIds = [];

  beforeAll(() => {
    client = new StateSetClient({
      apiKey: process.env.STATESET_TEST_API_KEY,
      environment: 'sandbox'
    });
    orderProcessor = new OrderProcessor(client);
  });

  afterAll(async () => {
    // Cleanup created orders
    for (const orderId of createdOrderIds) {
      try {
        await client.orders.delete(orderId);
      } catch (error) {
        console.warn(`Failed to cleanup order ${orderId}:`, error.message);
      }
    }
  });

  it('should create and retrieve an order', async () => {
    const orderData = {
      customer_email: 'integration-test@example.com',
      items: [
        {
          sku: 'TEST-ITEM-001',
          quantity: 2,
          price: 1999
        }
      ],
      shipping_address: {
        line1: '123 Test Street',
        city: 'Test City',
        state: 'CA',
        postal_code: '90210',
        country: 'US'
      }
    };

    // Create order
    const createdOrder = await orderProcessor.processOrder(orderData);
    createdOrderIds.push(createdOrder.id);

    expect(createdOrder.id).toBeDefined();
    expect(createdOrder.customer_email).toBe(orderData.customer_email);
    expect(createdOrder.status).toBe('pending');

    // Retrieve order
    const retrievedOrder = await client.orders.get(createdOrder.id);
    expect(retrievedOrder.id).toBe(createdOrder.id);
    expect(retrievedOrder.customer_email).toBe(orderData.customer_email);
  });

  it('should handle order status updates', async () => {
    const orderData = {
      customer_email: 'status-test@example.com',
      items: [{ sku: 'TEST-ITEM-002', quantity: 1, price: 999 }],
      shipping_address: {
        line1: '456 Status Street',
        city: 'Status City',
        state: 'NY',
        postal_code: '10001',
        country: 'US'
      }
    };

    const order = await orderProcessor.processOrder(orderData);
    createdOrderIds.push(order.id);

    // Update order status
    const updatedOrder = await client.orders.update(order.id, {
      status: 'confirmed'
    });

    expect(updatedOrder.status).toBe('confirmed');

    // Verify status change
    const retrievedOrder = await client.orders.get(order.id);
    expect(retrievedOrder.status).toBe('confirmed');
  });

  it('should handle order cancellation', async () => {
    const orderData = {
      customer_email: 'cancel-test@example.com',
      items: [{ sku: 'TEST-ITEM-003', quantity: 1, price: 1500 }],
      shipping_address: {
        line1: '789 Cancel Street',
        city: 'Cancel City',
        state: 'TX',
        postal_code: '75001',
        country: 'US'
      }
    };

    const order = await orderProcessor.processOrder(orderData);
    createdOrderIds.push(order.id);

    // Cancel order
    const cancelledOrder = await client.orders.cancel(order.id, {
      reason: 'Customer requested cancellation'
    });

    expect(cancelledOrder.status).toBe('cancelled');
    expect(cancelledOrder.cancellation_reason).toBe('Customer requested cancellation');
  });

  it('should handle API errors gracefully', async () => {
    const invalidOrderData = {
      customer_email: 'invalid-test@example.com',
      items: [
        {
          sku: 'INVALID-SKU-THAT-DOES-NOT-EXIST',
          quantity: 1,
          price: 999
        }
      ]
    };

    await expect(orderProcessor.processOrder(invalidOrderData)).rejects.toThrow();
  });

  it('should handle rate limiting', async () => {
    const requests = [];
    
    // Make multiple concurrent requests to test rate limiting
    for (let i = 0; i < 10; i++) {
      requests.push(
        client.orders.list({ limit: 1 })
      );
    }

    const results = await Promise.allSettled(requests);
    
    // At least some requests should succeed
    const successful = results.filter(r => r.status === 'fulfilled');
    expect(successful.length).toBeGreaterThan(0);
    
    // Check if any were rate limited
    const rateLimited = results.filter(
      r => r.status === 'rejected' && r.reason.status === 429
    );
    
    if (rateLimited.length > 0) {
      console.log(`${rateLimited.length} requests were rate limited (expected behavior)`);
    }
  });
});

End-to-End Testing

Complete Workflow Testing

// tests/e2e/order-workflow.e2e.test.js
import { test, expect } from '@playwright/test';
import { StateSetClient } from 'stateset-node';

test.describe('Complete Order Workflow', () => {
  let client;
  let testOrderId;

  test.beforeAll(async () => {
    client = new StateSetClient({
      apiKey: process.env.STATESET_TEST_API_KEY,
      environment: 'sandbox'
    });
  });

  test.afterAll(async () => {
    // Cleanup
    if (testOrderId) {
      try {
        await client.orders.delete(testOrderId);
      } catch (error) {
        console.warn('Failed to cleanup test order:', error.message);
      }
    }
  });

  test('should complete full order lifecycle', async ({ page }) => {
    // 1. Create order via API
    const orderData = {
      customer_email: 'e2e-test@example.com',
      items: [
        {
          sku: 'E2E-TEST-ITEM',
          quantity: 1,
          price: 2999,
          name: 'E2E Test Product'
        }
      ],
      shipping_address: {
        line1: '123 E2E Street',
        city: 'Test City',
        state: 'CA',
        postal_code: '90210',
        country: 'US'
      }
    };

    const order = await client.orders.create(orderData);
    testOrderId = order.id;
    
    expect(order.status).toBe('pending');

    // 2. Navigate to order management dashboard
    await page.goto(`${process.env.DASHBOARD_URL}/orders/${order.id}`);
    
    // 3. Verify order details in UI
    await expect(page.locator('[data-testid="order-id"]')).toContainText(order.id);
    await expect(page.locator('[data-testid="customer-email"]')).toContainText('e2e-test@example.com');
    await expect(page.locator('[data-testid="order-status"]')).toContainText('pending');

    // 4. Process order through UI
    await page.click('[data-testid="confirm-order-btn"]');
    await page.waitForSelector('[data-testid="order-status"]:has-text("confirmed")');

    // 5. Verify status change via API
    const updatedOrder = await client.orders.get(order.id);
    expect(updatedOrder.status).toBe('confirmed');

    // 6. Ship order
    await page.click('[data-testid="ship-order-btn"]');
    await page.fill('[data-testid="tracking-number"]', 'TEST-TRACKING-123');
    await page.click('[data-testid="confirm-shipment-btn"]');
    
    await page.waitForSelector('[data-testid="order-status"]:has-text("shipped")');

    // 7. Verify final status
    const shippedOrder = await client.orders.get(order.id);
    expect(shippedOrder.status).toBe('shipped');
    expect(shippedOrder.tracking_number).toBe('TEST-TRACKING-123');

    // 8. Check customer notification
    const notifications = await client.notifications.list({
      order_id: order.id,
      type: 'shipment'
    });
    
    expect(notifications.data.length).toBeGreaterThan(0);
    expect(notifications.data[0].recipient_email).toBe('e2e-test@example.com');
  });

  test('should handle order cancellation workflow', async ({ page }) => {
    // Create order
    const orderData = {
      customer_email: 'cancel-e2e-test@example.com',
      items: [{ sku: 'CANCEL-TEST-ITEM', quantity: 1, price: 1999 }],
      shipping_address: {
        line1: '456 Cancel Street',
        city: 'Cancel City',
        state: 'NY',
        postal_code: '10001',
        country: 'US'
      }
    };

    const order = await client.orders.create(orderData);
    testOrderId = order.id;

    // Navigate to order
    await page.goto(`${process.env.DASHBOARD_URL}/orders/${order.id}`);

    // Cancel order through UI
    await page.click('[data-testid="cancel-order-btn"]');
    await page.fill('[data-testid="cancellation-reason"]', 'Customer requested cancellation');
    await page.click('[data-testid="confirm-cancellation-btn"]');

    // Wait for status update
    await page.waitForSelector('[data-testid="order-status"]:has-text("cancelled")');

    // Verify via API
    const cancelledOrder = await client.orders.get(order.id);
    expect(cancelledOrder.status).toBe('cancelled');
    expect(cancelledOrder.cancellation_reason).toBe('Customer requested cancellation');
  });
});

Performance Testing

Load Testing with Artillery

# artillery-config.yml
config:
  target: 'https://api.sandbox.stateset.com'
  phases:
    - duration: 60
      arrivalRate: 5
      name: "Warm up"
    - duration: 120
      arrivalRate: 10
      name: "Load test"
    - duration: 60
      arrivalRate: 20
      name: "Stress test"
  variables:
    api_key: "{{ $env.STATESET_TEST_API_KEY }}"
  processor: "./test-helpers.js"

scenarios:
  - name: "Order Management"
    weight: 70
    flow:
      - post:
          url: "/v1/orders"
          headers:
            Authorization: "Bearer {{ api_key }}"
            Content-Type: "application/json"
          json:
            customer_email: "load-test-{{ $randomString() }}@example.com"
            items:
              - sku: "LOAD-TEST-ITEM"
                quantity: "{{ $randomInt(1, 5) }}"
                price: 1999
            shipping_address:
              line1: "123 Load Test St"
              city: "Test City"
              state: "CA"
              postal_code: "90210"
              country: "US"
          capture:
            - json: "$.id"
              as: "orderId"
      - get:
          url: "/v1/orders/{{ orderId }}"
          headers:
            Authorization: "Bearer {{ api_key }}"
      - put:
          url: "/v1/orders/{{ orderId }}"
          headers:
            Authorization: "Bearer {{ api_key }}"
            Content-Type: "application/json"
          json:
            status: "confirmed"

  - name: "Order Listing"
    weight: 30
    flow:
      - get:
          url: "/v1/orders"
          qs:
            limit: "{{ $randomInt(10, 50) }}"
            status: "pending"
          headers:
            Authorization: "Bearer {{ api_key }}"

Stress Testing

// stress-test.js
import { StateSetClient } from 'stateset-node';
import { performance } from 'perf_hooks';

class StressTest {
  constructor() {
    this.client = new StateSetClient({
      apiKey: process.env.STATESET_TEST_API_KEY,
      environment: 'sandbox'
    });
    this.metrics = {
      requests: 0,
      errors: 0,
      latencies: []
    };
  }

  async runConcurrentOrders(concurrency = 10, duration = 60000) {
    console.log(`Starting stress test: ${concurrency} concurrent users for ${duration}ms`);
    
    const startTime = Date.now();
    const workers = [];

    // Start concurrent workers
    for (let i = 0; i < concurrency; i++) {
      workers.push(this.orderWorker(startTime + duration));
    }

    // Wait for all workers to complete
    await Promise.all(workers);

    // Calculate results
    const totalDuration = Date.now() - startTime;
    const avgLatency = this.metrics.latencies.reduce((a, b) => a + b, 0) / this.metrics.latencies.length;
    const errorRate = (this.metrics.errors / this.metrics.requests) * 100;
    const throughput = this.metrics.requests / (totalDuration / 1000);

    console.log('Stress Test Results:');
    console.log(`Total Requests: ${this.metrics.requests}`);
    console.log(`Errors: ${this.metrics.errors} (${errorRate.toFixed(2)}%)`);
    console.log(`Average Latency: ${avgLatency.toFixed(2)}ms`);
    console.log(`Throughput: ${throughput.toFixed(2)} req/sec`);
    
    return {
      requests: this.metrics.requests,
      errors: this.metrics.errors,
      errorRate,
      avgLatency,
      throughput
    };
  }

  async orderWorker(endTime) {
    while (Date.now() < endTime) {
      const start = performance.now();
      
      try {
        await this.createTestOrder();
        this.metrics.requests++;
        this.metrics.latencies.push(performance.now() - start);
      } catch (error) {
        this.metrics.errors++;
        console.error('Order creation failed:', error.message);
      }

      // Small delay to prevent overwhelming
      await new Promise(resolve => setTimeout(resolve, 100));
    }
  }

  async createTestOrder() {
    const orderData = {
      customer_email: `stress-test-${Date.now()}@example.com`,
      items: [
        {
          sku: 'STRESS-TEST-ITEM',
          quantity: Math.floor(Math.random() * 5) + 1,
          price: Math.floor(Math.random() * 5000) + 1000
        }
      ],
      shipping_address: {
        line1: '123 Stress Test Street',
        city: 'Test City',
        state: 'CA',
        postal_code: '90210',
        country: 'US'
      }
    };

    return this.client.orders.create(orderData);
  }
}

// Run stress test
const stressTest = new StressTest();
stressTest.runConcurrentOrders(20, 120000) // 20 concurrent users for 2 minutes
  .then(results => {
    console.log('Stress test completed successfully');
    process.exit(0);
  })
  .catch(error => {
    console.error('Stress test failed:', error);
    process.exit(1);
  });

Testing Best Practices

Test Organization

  • Separate unit, integration, and E2E tests
  • Use descriptive test names
  • Group related tests together
  • Follow AAA pattern (Arrange, Act, Assert)

Test Data Management

  • Use factories for test data generation
  • Clean up test data after tests
  • Use isolated test environments
  • Mock external dependencies

CI/CD Integration

  • Run tests on every commit
  • Separate fast and slow test suites
  • Use parallel test execution
  • Generate coverage reports

Monitoring & Alerting

  • Monitor test execution times
  • Alert on test failures
  • Track test coverage trends
  • Performance regression detection

Continuous Integration Setup

# .github/workflows/test.yml
name: Test Suite

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - run: npm ci
      - run: npm run test:unit
        env:
          STATESET_TEST_API_KEY: ${{ secrets.STATESET_TEST_API_KEY }}
      
      - name: Upload coverage reports
        uses: codecov/codecov-action@v3

  integration-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - run: npm ci
      - run: npm run test:integration
        env:
          STATESET_TEST_API_KEY: ${{ secrets.STATESET_TEST_API_KEY }}

  e2e-tests:
    runs-on: ubuntu-latest
    needs: integration-tests
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - run: npm ci
      - run: npx playwright install
      - run: npm run test:e2e
        env:
          STATESET_TEST_API_KEY: ${{ secrets.STATESET_TEST_API_KEY }}
          DASHBOARD_URL: ${{ secrets.DASHBOARD_URL }}

Next Steps

After implementing comprehensive testing:

  1. Monitor Test Metrics: Track coverage, execution time, and flakiness
  2. Expand Test Scenarios: Add edge cases and error conditions
  3. Performance Baselines: Establish performance benchmarks
  4. Test Automation: Integrate with deployment pipelines

For more testing strategies, see: