As software architectures evolve from monolithic applications to distributed microservices, API testing (as discussed in GraphQL Testing: Complete Guide with Examples) has become increasingly complex and critical. Modern systems rely on diverse communication protocols—REST, GraphQL (as discussed in API Testing Mastery: From REST to Contract Testing), WebSockets, Server-Sent Events—each requiring specialized testing approaches. This comprehensive guide explores cutting-edge API testing strategies for microservices architectures, covering everything from basic contract testing (as discussed in Contract Testing: Painless Microservices Communication) to advanced versioning strategies.

The Modern API Testing Landscape

API testing in 2025 encompasses far more than simple REST endpoint validation. Modern testing strategies must address:

  • Distributed architectures: Testing interactions across dozens or hundreds of microservices
  • Multiple protocols: REST, GraphQL, gRPC, WebSockets, SSE, and more
  • Asynchronous communication: Message queues, event streams, and webhooks
  • Contract compliance: Ensuring consumer-provider compatibility
  • Performance at scale: Testing under realistic load conditions
  • Security considerations: Authentication, authorization, encryption, and rate limiting

Microservices Testing Strategies

The Testing Pyramid for Microservices

           ╱‾‾‾‾‾‾‾‾‾‾‾╲
          ╱  End-to-End ╲
         ╱     Tests     ╲       5-10% (Critical business flows)
        ╱─────────────────╲
       ╱   Contract Tests  ╲
      ╱    (Consumer &      ╲     20-30% (Service boundaries)
     ╱      Provider)        ╲
    ╱─────────────────────────╲
   ╱   Integration Tests       ╲
  ╱  (Within service scope)     ╲   30-40% (Internal interactions)
 ╱───────────────────────────────╲
╱        Unit Tests               ╲  40-50% (Business logic)
╲─────────────────────────────────╱

Component Testing: Isolating Microservices

Component tests validate a single microservice in isolation, mocking all external dependencies.

Example: Testing User Service with Mocked Dependencies

// user-service.test.js
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
import request from 'supertest';
import { createApp } from '../src/app.js';
import { MockAuthService } from './mocks/auth-service.js';
import { MockDatabaseClient } from './mocks/database.js';

describe('User Service API', () => {
  let app;
  let mockAuth;
  let mockDb;

  beforeAll(async () => {
    // Initialize mocks
    mockAuth = new MockAuthService();
    mockDb = new MockDatabaseClient();

    // Create app with mocked dependencies
    app = await createApp({
      authService: mockAuth,
      database: mockDb
    });

    // Seed test data
    await mockDb.seed({
      users: [
        { id: '1', email: 'user@example.com', role: 'admin' },
        { id: '2', email: 'user2@example.com', role: 'user' }
      ]
    });
  });

  afterAll(async () => {
    await mockDb.cleanup();
  });

  describe('GET /api/users/:id', () => {
    it('should return user when authenticated', async () => {
      // Set up mock authentication
      mockAuth.setValidToken('valid-token-123');

      const response = await request(app)
        .get('/api/users/1')
        .set('Authorization', 'Bearer valid-token-123')
        .expect(200);

      expect(response.body).toMatchObject({
        id: '1',
        email: 'user@example.com',
        role: 'admin'
      });

      // Verify auth service was called
      expect(mockAuth.validateToken).toHaveBeenCalledWith('valid-token-123');
    });

    it('should return 401 when token is invalid', async () => {
      mockAuth.setInvalidToken();

      await request(app)
        .get('/api/users/1')
        .set('Authorization', 'Bearer invalid-token')
        .expect(401);
    });

    it('should return 404 when user does not exist', async () => {
      mockAuth.setValidToken('valid-token-123');

      await request(app)
        .get('/api/users/999')
        .set('Authorization', 'Bearer valid-token-123')
        .expect(404);
    });
  });

  describe('POST /api/users', () => {
    it('should create user with valid data', async () => {
      mockAuth.setValidToken('admin-token');
      mockAuth.setRole('admin');

      const newUser = {
        email: 'newuser@example.com',
        password: 'SecureP@ss123!',
        role: 'user'
      };

      const response = await request(app)
        .post('/api/users')
        .set('Authorization', 'Bearer admin-token')
        .send(newUser)
        .expect(201);

      expect(response.body).toMatchObject({
        email: 'newuser@example.com',
        role: 'user'
      });
      expect(response.body.password).toBeUndefined(); // Password should not be returned
    });

    it('should validate email format', async () => {
      mockAuth.setValidToken('admin-token');
      mockAuth.setRole('admin');

      await request(app)
        .post('/api/users')
        .set('Authorization', 'Bearer admin-token')
        .send({
          email: 'invalid-email',
          password: 'SecureP@ss123!'
        })
        .expect(400)
        .expect((res) => {
          expect(res.body.errors).toContainEqual(
            expect.objectContaining({
              field: 'email',
              message: expect.stringContaining('valid email')
            })
          );
        });
    });
  });
});

Integration Testing: Service-to-Service Communication

Integration tests verify interactions between multiple microservices in a controlled environment.

Example: Testing Order Service with Real Payment Gateway

import pytest
import requests
from testcontainers.compose import DockerCompose

@pytest.fixture(scope="module")
def services():
    """Start services using Docker Compose"""
    compose = DockerCompose(".", compose_file_name="docker-compose.test.yml")
    compose.start()

    # Wait for services to be healthy
    compose.wait_for("http://localhost:8001/health")  # Order service
    compose.wait_for("http://localhost:8002/health")  # Payment service
    compose.wait_for("http://localhost:8003/health")  # Inventory service

    yield {
        'order_service': 'http://localhost:8001',
        'payment_service': 'http://localhost:8002',
        'inventory_service': 'http://localhost:8003'
    }

    compose.stop()

class TestOrderFlow:
    def test_successful_order_creation(self, services):
        """Test complete order flow with payment and inventory"""
        # 1. Check inventory availability
        inventory_check = requests.get(
            f"{services['inventory_service']}/api/inventory/product-123"
        )
        assert inventory_check.status_code == 200
        assert inventory_check.json()['available_quantity'] >= 1

        # 2. Create order
        order_data = {
            'customer_id': 'cust-456',
            'items': [
                {'product_id': 'product-123', 'quantity': 1, 'price': 99.99}
            ],
            'payment_method': 'credit_card',
            'card_token': 'tok_visa_test_card'
        }

        order_response = requests.post(
            f"{services['order_service']}/api/orders",
            json=order_data,
            headers={'Authorization': 'Bearer test-token'}
        )

        assert order_response.status_code == 201
        order = order_response.json()
        assert order['status'] == 'confirmed'
        assert order['payment_status'] == 'paid'

        # 3. Verify inventory was decremented
        updated_inventory = requests.get(
            f"{services['inventory_service']}/api/inventory/product-123"
        )
        original_qty = inventory_check.json()['available_quantity']
        new_qty = updated_inventory.json()['available_quantity']
        assert new_qty == original_qty - 1

        # 4. Verify payment was processed
        payment_response = requests.get(
            f"{services['payment_service']}/api/payments/order/{order['id']}",
            headers={'Authorization': 'Bearer test-token'}
        )
        assert payment_response.status_code == 200
        payment = payment_response.json()
        assert payment['amount'] == 99.99
        assert payment['status'] == 'completed'

    def test_order_fails_when_payment_declined(self, services):
        """Test order rollback when payment fails"""
        order_data = {
            'customer_id': 'cust-456',
            'items': [
                {'product_id': 'product-123', 'quantity': 1, 'price': 99.99}
            ],
            'payment_method': 'credit_card',
            'card_token': 'tok_chargeDeclined'  # Test card that fails
        }

        # Get initial inventory
        initial_inventory = requests.get(
            f"{services['inventory_service']}/api/inventory/product-123"
        ).json()['available_quantity']

        # Attempt to create order
        order_response = requests.post(
            f"{services['order_service']}/api/orders",
            json=order_data,
            headers={'Authorization': 'Bearer test-token'}
        )

        assert order_response.status_code == 402  # Payment Required
        assert order_response.json()['payment_status'] == 'declined'

        # Verify inventory was NOT decremented (rollback)
        final_inventory = requests.get(
            f"{services['inventory_service']}/api/inventory/product-123"
        ).json()['available_quantity']
        assert final_inventory == initial_inventory

    def test_order_fails_when_inventory_insufficient(self, services):
        """Test order rejection when inventory is unavailable"""
        order_data = {
            'customer_id': 'cust-456',
            'items': [
                {'product_id': 'product-123', 'quantity': 10000}  # Excessive quantity
            ],
            'payment_method': 'credit_card',
            'card_token': 'tok_visa_test_card'
        }

        order_response = requests.post(
            f"{services['order_service']}/api/orders",
            json=order_data,
            headers={'Authorization': 'Bearer test-token'}
        )

        assert order_response.status_code == 409  # Conflict
        assert 'insufficient inventory' in order_response.json()['error'].lower()

Service Mesh Testing

For microservices using service mesh (Istio, Linkerd), test traffic management and resilience patterns:

# chaos-testing.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
  name: test-payment-service-delay
spec:
  action: delay
  mode: all
  selector:
    namespaces:
      - default
    labelSelectors:
      app: payment-service
  delay:
    latency: "500ms"
    correlation: "100"
    jitter: "0ms"
  duration: "5m"
# Test resilience to network delays
def test_order_service_handles_payment_delays():
    """Verify order service timeout and retry logic"""
    # Apply chaos engineering scenario
    apply_chaos("test-payment-service-delay")

    start_time = time.time()
    response = requests.post(
        "http://order-service/api/orders",
        json=order_data,
        timeout=10
    )
    elapsed = time.time() - start_time

    # Should implement circuit breaker and timeout
    assert elapsed < 10, "Service should fail fast, not hang"
    assert response.status_code in [503, 504], "Should return timeout error"

    cleanup_chaos("test-payment-service-delay")

GraphQL-Specific Testing

GraphQL requires different testing strategies compared to REST APIs due to its flexible query structure and type system.

Schema Testing

import { buildSchema } from 'graphql';
import { describe, it, expect } from '@jest/globals';
import fs from 'fs';

describe('GraphQL Schema Validation', () => {
  it('should have valid schema syntax', () => {
    const schemaString = fs.readFileSync('./schema.graphql', 'utf8');

    expect(() => {
      buildSchema(schemaString);
    }).not.toThrow();
  });

  it('should define required types', () => {
    const schema = buildSchema(
      fs.readFileSync('./schema.graphql', 'utf8')
    );

    const typeMap = schema.getTypeMap();

    // Verify core types exist
    expect(typeMap).toHaveProperty('User');
    expect(typeMap).toHaveProperty('Order');
    expect(typeMap).toHaveProperty('Product');
    expect(typeMap).toHaveProperty('Query');
    expect(typeMap).toHaveProperty('Mutation');
  });

  it('should have documented fields', () => {
    const schema = buildSchema(
      fs.readFileSync('./schema.graphql', 'utf8')
    );

    const userType = schema.getType('User');
    const fields = userType.getFields();

    // Verify critical fields are documented
    Object.keys(fields).forEach(fieldName => {
      const field = fields[fieldName];
      expect(field.description).toBeTruthy();
      expect(field.description.length).toBeGreaterThan(10);
    });
  });
});

Query Testing

import { graphql } from 'graphql';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { describe, it, expect, beforeEach } from '@jest/globals';

describe('GraphQL Queries', () => {
  let schema;
  let context;

  beforeEach(() => {
    schema = makeExecutableSchema({
      typeDefs,
      resolvers
    });

    context = {
      userId: 'user-123',
      db: mockDatabase,
      services: mockServices
    };
  });

  describe('User queries', () => {
    it('should fetch user with selected fields', async () => {
      const query = `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            email
            profile {
              firstName
              lastName
            }
          }
        }
      `;

      const result = await graphql({
        schema,
        source: query,
        variableValues: { id: 'user-123' },
        contextValue: context
      });

      expect(result.errors).toBeUndefined();
      expect(result.data.user).toMatchObject({
        id: 'user-123',
        email: 'user@example.com',
        profile: {
          firstName: 'John',
          lastName: 'Doe'
        }
      });
    });

    it('should handle nested queries efficiently (N+1 problem)', async () => {
      const query = `
        query GetUsersWithOrders {
          users(limit: 10) {
            id
            email
            orders {
              id
              total
              items {
                id
                product {
                  name
                  price
                }
              }
            }
          }
        }
      `;

      // Track database queries
      const dbSpy = jest.spyOn(context.db, 'query');

      const result = await graphql({
        schema,
        source: query,
        contextValue: context
      });

      expect(result.errors).toBeUndefined();

      // Verify DataLoader batching is working
      // Should make ~4 queries, not 10+ (N+1 problem)
      expect(dbSpy.mock.calls.length).toBeLessThan(5);
    });

    it('should enforce field-level authorization', async () => {
      const query = `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            email
            privateData {
              ssn
              creditCards
            }
          }
        }
      `;

      // Context without admin role
      const limitedContext = {
        ...context,
        userId: 'user-456',
        roles: ['user']
      };

      const result = await graphql({
        schema,
        source: query,
        variableValues: { id: 'user-123' },
        contextValue: limitedContext
      });

      expect(result.errors).toBeDefined();
      expect(result.errors[0].message).toMatch(/not authorized/i);
    });
  });

  describe('Mutations', () => {
    it('should create user with valid input', async () => {
      const mutation = `
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            user {
              id
              email
              profile {
                firstName
                lastName
              }
            }
            errors {
              field
              message
            }
          }
        }
      `;

      const input = {
        email: 'newuser@example.com',
        password: 'SecureP@ss123!',
        profile: {
          firstName: 'Jane',
          lastName: 'Smith'
        }
      };

      const result = await graphql({
        schema,
        source: mutation,
        variableValues: { input },
        contextValue: context
      });

      expect(result.errors).toBeUndefined();
      expect(result.data.createUser.errors).toHaveLength(0);
      expect(result.data.createUser.user).toMatchObject({
        email: 'newuser@example.com',
        profile: {
          firstName: 'Jane',
          lastName: 'Smith'
        }
      });
    });

    it('should validate input and return structured errors', async () => {
      const mutation = `
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            user {
              id
            }
            errors {
              field
              message
            }
          }
        }
      `;

      const invalidInput = {
        email: 'invalid-email',
        password: 'weak'
      };

      const result = await graphql({
        schema,
        source: mutation,
        variableValues: { input: invalidInput },
        contextValue: context
      });

      expect(result.data.createUser.user).toBeNull();
      expect(result.data.createUser.errors).toContainEqual(
        expect.objectContaining({
          field: 'email',
          message: expect.stringContaining('valid email')
        })
      );
      expect(result.data.createUser.errors).toContainEqual(
        expect.objectContaining({
          field: 'password',
          message: expect.stringContaining('at least 8 characters')
        })
      );
    });
  });
});

GraphQL Performance Testing

import { graphql } from 'graphql';

describe('GraphQL Performance', () => {
  it('should limit query depth to prevent DoS', async () => {
    // Attempt deeply nested query
    const maliciousQuery = `
      query DeepNesting {
        user(id: "1") {
          friends {
            friends {
              friends {
                friends {
                  friends {
                    friends {
                      id
                    }
                  }
                }
              }
            }
          }
        }
      }
    `;

    const result = await graphql({
      schema,
      source: maliciousQuery,
      contextValue: context
    });

    expect(result.errors).toBeDefined();
    expect(result.errors[0].message).toMatch(/query depth|too deep/i);
  });

  it('should enforce query complexity limits', async () => {
    const complexQuery = `
      query ExpensiveQuery {
        users(limit: 1000) {
          id
          orders(limit: 1000) {
            id
            items(limit: 1000) {
              id
              product {
                reviews(limit: 1000) {
                  id
                  author {
                    id
                  }
                }
              }
            }
          }
        }
      }
    `;

    const result = await graphql({
      schema,
      source: complexQuery,
      contextValue: context
    });

    expect(result.errors).toBeDefined();
    expect(result.errors[0].message).toMatch(/query complexity|too complex/i);
  });
});

WebSocket and Server-Sent Events Testing

WebSocket Testing

import WebSocket from 'ws';
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';

describe('WebSocket API', () => {
  let wsServer;
  let baseUrl;

  beforeAll(async () => {
    wsServer = await startWebSocketServer();
    baseUrl = `ws://localhost:${wsServer.port}`;
  });

  afterAll(async () => {
    await wsServer.close();
  });

  it('should establish connection and authenticate', (done) => {
    const ws = new WebSocket(`${baseUrl}/ws`);

    ws.on('open', () => {
      // Send authentication message
      ws.send(JSON.stringify({
        type: 'auth',
        token: 'valid-token-123'
      }));
    });

    ws.on('message', (data) => {
      const message = JSON.parse(data.toString());

      if (message.type === 'auth_success') {
        expect(message.userId).toBe('user-123');
        ws.close();
        done();
      }
    });

    ws.on('error', done);
  });

  it('should receive real-time updates', (done) => {
    const ws = new WebSocket(`${baseUrl}/ws`);
    const receivedMessages = [];

    ws.on('open', () => {
      ws.send(JSON.stringify({
        type: 'auth',
        token: 'valid-token-123'
      }));
    });

    ws.on('message', (data) => {
      const message = JSON.parse(data.toString());
      receivedMessages.push(message);

      if (message.type === 'auth_success') {
        // Subscribe to updates
        ws.send(JSON.stringify({
          type: 'subscribe',
          channel: 'orders'
        }));
      }

      if (message.type === 'order_update') {
        expect(message.data).toHaveProperty('orderId');
        expect(message.data).toHaveProperty('status');
        ws.close();
        done();
      }
    });

    // Trigger an order update from another client
    setTimeout(() => {
      triggerOrderUpdate('order-123', 'shipped');
    }, 100);
  });

  it('should handle connection drops and reconnection', async () => {
    const ws = new WebSocket(`${baseUrl}/ws`);
    const messages = [];

    await new Promise((resolve) => {
      ws.on('open', () => {
        ws.send(JSON.stringify({
          type: 'auth',
          token: 'valid-token-123'
        }));
      });

      ws.on('message', (data) => {
        messages.push(JSON.parse(data.toString()));
        if (messages.length === 1) resolve();
      });
    });

    // Simulate connection drop
    ws.close();

    // Wait a bit
    await new Promise(resolve => setTimeout(resolve, 100));

    // Reconnect
    const ws2 = new WebSocket(`${baseUrl}/ws`);

    await new Promise((resolve, reject) => {
      ws2.on('open', () => {
        ws2.send(JSON.stringify({
          type: 'auth',
          token: 'valid-token-123'
        }));
      });

      ws2.on('message', (data) => {
        const message = JSON.parse(data.toString());
        if (message.type === 'auth_success') {
          expect(message.sessionRestored).toBe(true);
          ws2.close();
          resolve();
        }
      });

      ws2.on('error', reject);
    });
  });

  it('should enforce rate limiting', async () => {
    const ws = new WebSocket(`${baseUrl}/ws`);

    await new Promise((resolve) => {
      ws.on('open', resolve);
    });

    // Authenticate first
    ws.send(JSON.stringify({
      type: 'auth',
      token: 'valid-token-123'
    }));

    await new Promise((resolve) => {
      ws.on('message', (data) => {
        const message = JSON.parse(data.toString());
        if (message.type === 'auth_success') resolve();
      });
    });

    // Send many messages rapidly
    const promises = [];
    for (let i = 0; i < 100; i++) {
      ws.send(JSON.stringify({
        type: 'ping',
        id: i
      }));
    }

    // Should receive rate limit error
    await new Promise((resolve) => {
      ws.on('message', (data) => {
        const message = JSON.parse(data.toString());
        if (message.type === 'rate_limit_exceeded') {
          expect(message.retryAfter).toBeGreaterThan(0);
          ws.close();
          resolve();
        }
      });
    });
  });
});

Server-Sent Events (SSE) Testing

import { describe, it, expect } from '@jest/globals';
import EventSource from 'eventsource';

describe('Server-Sent Events API', () => {
  it('should stream real-time notifications', (done) => {
    const eventSource = new EventSource(
      'http://localhost:3000/api/notifications/stream',
      {
        headers: {
          'Authorization': 'Bearer valid-token-123'
        }
      }
    );

    const receivedEvents = [];

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      receivedEvents.push(data);

      if (receivedEvents.length >= 3) {
        eventSource.close();

        expect(receivedEvents).toHaveLength(3);
        expect(receivedEvents[0]).toHaveProperty('type');
        expect(receivedEvents[0]).toHaveProperty('timestamp');
        done();
      }
    };

    eventSource.onerror = (error) => {
      eventSource.close();
      done(error);
    };

    // Trigger some notifications
    setTimeout(() => {
      triggerNotification('user-123', 'order_shipped');
      triggerNotification('user-123', 'payment_processed');
      triggerNotification('user-123', 'message_received');
    }, 100);
  });

  it('should handle custom event types', (done) => {
    const eventSource = new EventSource(
      'http://localhost:3000/api/analytics/stream',
      {
        headers: {
          'Authorization': 'Bearer valid-token-123'
        }
      }
    );

    eventSource.addEventListener('metric_update', (event) => {
      const data = JSON.parse(event.data);

      expect(data).toHaveProperty('metricName');
      expect(data).toHaveProperty('value');
      expect(data).toHaveProperty('timestamp');

      eventSource.close();
      done();
    });

    eventSource.onerror = (error) => {
      eventSource.close();
      done(error);
    };
  });

  it('should reconnect automatically on connection loss', async () => {
    const eventSource = new EventSource(
      'http://localhost:3000/api/notifications/stream',
      {
        headers: {
          'Authorization': 'Bearer valid-token-123'
        }
      }
    );

    let connectionAttempts = 0;

    eventSource.addEventListener('open', () => {
      connectionAttempts++;
    });

    // Wait for initial connection
    await new Promise(resolve => setTimeout(resolve, 100));
    expect(connectionAttempts).toBe(1);

    // Simulate server restart
    await restartServer();

    // Wait for reconnection
    await new Promise(resolve => setTimeout(resolve, 3000));

    // Should have reconnected
    expect(connectionAttempts).toBeGreaterThan(1);

    eventSource.close();
  });
});

API Versioning Strategies

URL Versioning Testing

describe('API Versioning', () => {
  describe('V1 API', () => {
    it('should return data in v1 format', async () => {
      const response = await fetch('http://localhost:3000/api/v1/users/123');
      const user = await response.json();

      // V1 format: flat structure
      expect(user).toMatchObject({
        id: '123',
        name: 'John Doe',
        email: 'john@example.com'
      });
    });
  });

  describe('V2 API', () => {
    it('should return data in v2 format with nested structure', async () => {
      const response = await fetch('http://localhost:3000/api/v2/users/123');
      const user = await response.json();

      // V2 format: nested structure with profile
      expect(user).toMatchObject({
        id: '123',
        profile: {
          firstName: 'John',
          lastName: 'Doe'
        },
        contact: {
          email: 'john@example.com'
        }
      });
    });

    it('should include new fields not in v1', async () => {
      const response = await fetch('http://localhost:3000/api/v2/users/123');
      const user = await response.json();

      expect(user).toHaveProperty('metadata');
      expect(user).toHaveProperty('preferences');
      expect(user).toHaveProperty('createdAt');
      expect(user).toHaveProperty('updatedAt');
    });
  });

  describe('Version deprecation', () => {
    it('should include deprecation headers in v1 responses', async () => {
      const response = await fetch('http://localhost:3000/api/v1/users/123');

      expect(response.headers.get('Deprecation')).toBe('true');
      expect(response.headers.get('Sunset')).toBeTruthy();
      expect(response.headers.get('Link')).toContain('api/v2');
    });
  });
});

Header-Based Versioning Testing

describe('Header-based API Versioning', () => {
  it('should return v1 format with v1 accept header', async () => {
    const response = await fetch('http://localhost:3000/api/users/123', {
      headers: {
        'Accept': 'application/vnd.myapi.v1+json'
      }
    });

    const user = await response.json();
    expect(user.name).toBeDefined(); // V1 uses 'name'
    expect(user.profile).toBeUndefined(); // V2 feature
  });

  it('should return v2 format with v2 accept header', async () => {
    const response = await fetch('http://localhost:3000/api/users/123', {
      headers: {
        'Accept': 'application/vnd.myapi.v2+json'
      }
    });

    const user = await response.json();
    expect(user.profile).toBeDefined(); // V2 feature
    expect(user.name).toBeUndefined(); // V1 field removed
  });

  it('should default to latest version without header', async () => {
    const response = await fetch('http://localhost:3000/api/users/123');
    const user = await response.json();

    // Should use V2 format
    expect(user.profile).toBeDefined();
  });
});

Breaking Changes Testing

describe('API Breaking Changes', () => {
  it('should maintain backward compatibility in v1', async () => {
    // Old client code expecting v1 format
    const response = await fetch('http://localhost:3000/api/v1/orders/456');
    const order = await response.json();

    // V1 contract must be maintained
    expect(order).toHaveProperty('customerId');
    expect(order).toHaveProperty('items');
    expect(order).toHaveProperty('totalAmount');
    expect(typeof order.totalAmount).toBe('number');
  });

  it('should document breaking changes in v2', async () => {
    const response = await fetch('http://localhost:3000/api/v2/orders/456');
    const order = await response.json();

    // V2 breaking changes:
    // - customerId renamed to customer.id
    // - totalAmount changed to money object
    expect(order).not.toHaveProperty('customerId');
    expect(order).toHaveProperty('customer');
    expect(order.customer).toHaveProperty('id');

    expect(typeof order.totalAmount).toBe('object');
    expect(order.totalAmount).toHaveProperty('amount');
    expect(order.totalAmount).toHaveProperty('currency');
  });
});

Conclusion

Modern API testing requires a sophisticated approach that goes beyond simple endpoint validation. Whether you’re testing microservices architectures with complex inter-service communication, implementing GraphQL with its flexible query structures, working with real-time protocols like WebSockets and SSE, or managing API versioning strategies, success depends on comprehensive testing at every level.

Key Takeaways:

  1. Microservices testing requires a balanced pyramid: unit tests, integration tests, contract tests, and minimal E2E tests
  2. GraphQL testing must address schema validation, query performance, authorization, and N+1 query problems
  3. WebSocket and SSE testing requires handling asynchronous communication, connection management, and real-time data validation
  4. API versioning needs careful testing to ensure backward compatibility while allowing evolution
  5. Automation and CI/CD integration are essential for maintaining quality at scale

By implementing these strategies, teams can build reliable, performant, and maintainable API architectures that scale with business needs.