Skip to main content

Testing Strategies

Testing microservices is challenging due to distributed nature and service dependencies. Learn comprehensive testing strategies at every level.
Learning Objectives:
  • Implement unit testing with mocks
  • Write integration tests for services
  • Use contract testing with Pact
  • Design end-to-end test strategies
  • Set up test environments

Testing Pyramid for Microservices

┌─────────────────────────────────────────────────────────────────────────────┐
│                    MICROSERVICES TESTING PYRAMID                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│                              ▲                                               │
│                             ╱ ╲                                              │
│                            ╱   ╲                                             │
│                           ╱ E2E ╲        Few, slow, expensive               │
│                          ╱───────╲       Test critical user journeys         │
│                         ╱         ╲                                          │
│                        ╱ Component ╲     Service in isolation                │
│                       ╱─────────────╲    with real DB, mocked deps          │
│                      ╱               ╲                                       │
│                     ╱    Contract     ╲  Verify service compatibility       │
│                    ╱───────────────────╲ Consumer-driven contracts          │
│                   ╱                     ╲                                    │
│                  ╱     Integration       ╲  Test with real dependencies     │
│                 ╱─────────────────────────╲ Database, message queue         │
│                ╱                           ╲                                 │
│               ╱           Unit              ╲  Fast, isolated               │
│              ╱───────────────────────────────╲ Many, quick, cheap           │
│                                                                              │
│                                                                              │
│  ┌─────────────┬────────────┬────────────┬───────────────────────────────┐  │
│  │   Level     │   Count    │   Speed    │   What to Test                │  │
│  ├─────────────┼────────────┼────────────┼───────────────────────────────┤  │
│  │ Unit        │ Thousands  │ < 1ms      │ Business logic, utilities     │  │
│  │ Integration │ Hundreds   │ < 1s       │ DB queries, external calls    │  │
│  │ Contract    │ Tens       │ < 5s       │ API compatibility             │  │
│  │ Component   │ Tens       │ < 30s      │ Service endpoints             │  │
│  │ E2E         │ Few        │ < 5min     │ Critical paths                │  │
│  └─────────────┴────────────┴────────────┴───────────────────────────────┘  │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Unit Testing

Testing Business Logic

// services/OrderService.js
class OrderService {
  constructor(orderRepository, pricingService, inventoryClient) {
    this.orderRepository = orderRepository;
    this.pricingService = pricingService;
    this.inventoryClient = inventoryClient;
  }

  async createOrder(customerId, items) {
    // Validate items
    if (!items || items.length === 0) {
      throw new ValidationError('Order must have at least one item');
    }

    if (items.length > 50) {
      throw new ValidationError('Order cannot have more than 50 items');
    }

    // Check inventory
    const availability = await this.inventoryClient.checkAvailability(items);
    const unavailable = availability.filter(item => !item.available);
    
    if (unavailable.length > 0) {
      throw new InsufficientInventoryError(unavailable);
    }

    // Calculate pricing
    const pricing = await this.pricingService.calculate(items, customerId);

    // Create order
    const order = {
      id: generateId(),
      customerId,
      items,
      subtotal: pricing.subtotal,
      tax: pricing.tax,
      total: pricing.total,
      status: 'PENDING',
      createdAt: new Date()
    };

    await this.orderRepository.save(order);
    
    return order;
  }

  calculateDiscount(order, discountCode) {
    const discounts = {
      'SAVE10': { type: 'percentage', value: 10 },
      'FLAT50': { type: 'fixed', value: 50 },
      'FREESHIP': { type: 'shipping', value: 0 }
    };

    const discount = discounts[discountCode];
    if (!discount) {
      throw new InvalidDiscountError(discountCode);
    }

    switch (discount.type) {
      case 'percentage':
        return order.subtotal * (discount.value / 100);
      case 'fixed':
        return Math.min(discount.value, order.subtotal);
      case 'shipping':
        return order.shippingCost;
      default:
        return 0;
    }
  }
}

Unit Tests with Jest

// tests/unit/OrderService.test.js
const { OrderService } = require('../../services/OrderService');
const { ValidationError, InsufficientInventoryError } = require('../../errors');

describe('OrderService', () => {
  let orderService;
  let mockOrderRepository;
  let mockPricingService;
  let mockInventoryClient;

  beforeEach(() => {
    // Create mocks
    mockOrderRepository = {
      save: jest.fn().mockResolvedValue(undefined),
      findById: jest.fn()
    };

    mockPricingService = {
      calculate: jest.fn().mockResolvedValue({
        subtotal: 100,
        tax: 10,
        total: 110
      })
    };

    mockInventoryClient = {
      checkAvailability: jest.fn().mockResolvedValue([
        { productId: 'p1', available: true },
        { productId: 'p2', available: true }
      ])
    };

    orderService = new OrderService(
      mockOrderRepository,
      mockPricingService,
      mockInventoryClient
    );
  });

  describe('createOrder', () => {
    const customerId = 'customer-123';
    const validItems = [
      { productId: 'p1', quantity: 2 },
      { productId: 'p2', quantity: 1 }
    ];

    it('should create order with valid items', async () => {
      const order = await orderService.createOrder(customerId, validItems);

      expect(order).toMatchObject({
        customerId,
        items: validItems,
        subtotal: 100,
        tax: 10,
        total: 110,
        status: 'PENDING'
      });
      expect(order.id).toBeDefined();
      expect(mockOrderRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({ customerId })
      );
    });

    it('should throw ValidationError for empty items', async () => {
      await expect(
        orderService.createOrder(customerId, [])
      ).rejects.toThrow(ValidationError);

      await expect(
        orderService.createOrder(customerId, [])
      ).rejects.toThrow('Order must have at least one item');
    });

    it('should throw ValidationError for too many items', async () => {
      const tooManyItems = Array(51).fill({ productId: 'p1', quantity: 1 });

      await expect(
        orderService.createOrder(customerId, tooManyItems)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw InsufficientInventoryError when items unavailable', async () => {
      mockInventoryClient.checkAvailability.mockResolvedValue([
        { productId: 'p1', available: true },
        { productId: 'p2', available: false }
      ]);

      await expect(
        orderService.createOrder(customerId, validItems)
      ).rejects.toThrow(InsufficientInventoryError);
    });

    it('should check inventory before calculating pricing', async () => {
      await orderService.createOrder(customerId, validItems);

      expect(mockInventoryClient.checkAvailability).toHaveBeenCalledBefore(
        mockPricingService.calculate
      );
    });
  });

  describe('calculateDiscount', () => {
    const order = { subtotal: 200, shippingCost: 15 };

    it('should apply percentage discount correctly', () => {
      const discount = orderService.calculateDiscount(order, 'SAVE10');
      expect(discount).toBe(20); // 10% of 200
    });

    it('should apply fixed discount correctly', () => {
      const discount = orderService.calculateDiscount(order, 'FLAT50');
      expect(discount).toBe(50);
    });

    it('should not exceed subtotal for fixed discount', () => {
      const smallOrder = { subtotal: 30 };
      const discount = orderService.calculateDiscount(smallOrder, 'FLAT50');
      expect(discount).toBe(30);
    });

    it('should apply shipping discount correctly', () => {
      const discount = orderService.calculateDiscount(order, 'FREESHIP');
      expect(discount).toBe(15);
    });

    it('should throw for invalid discount code', () => {
      expect(() => {
        orderService.calculateDiscount(order, 'INVALID');
      }).toThrow('Invalid discount code');
    });
  });
});

Integration Testing

Database Integration Tests

// tests/integration/OrderRepository.test.js
const { OrderRepository } = require('../../repositories/OrderRepository');
const { createTestDatabase, closeTestDatabase } = require('../helpers/database');

describe('OrderRepository Integration', () => {
  let orderRepository;
  let db;

  beforeAll(async () => {
    db = await createTestDatabase();
    orderRepository = new OrderRepository(db);
  });

  afterAll(async () => {
    await closeTestDatabase(db);
  });

  beforeEach(async () => {
    await db.query('DELETE FROM orders');
    await db.query('DELETE FROM order_items');
  });

  describe('save', () => {
    it('should persist order to database', async () => {
      const order = {
        id: 'order-123',
        customerId: 'customer-456',
        items: [{ productId: 'p1', quantity: 2, price: 50 }],
        total: 100,
        status: 'PENDING'
      };

      await orderRepository.save(order);

      const result = await db.query(
        'SELECT * FROM orders WHERE id = $1',
        [order.id]
      );

      expect(result.rows[0]).toMatchObject({
        id: order.id,
        customer_id: order.customerId,
        total: '100.00',
        status: 'PENDING'
      });
    });

    it('should save order items', async () => {
      const order = {
        id: 'order-123',
        customerId: 'customer-456',
        items: [
          { productId: 'p1', quantity: 2, price: 50 },
          { productId: 'p2', quantity: 1, price: 30 }
        ],
        total: 130,
        status: 'PENDING'
      };

      await orderRepository.save(order);

      const result = await db.query(
        'SELECT * FROM order_items WHERE order_id = $1',
        [order.id]
      );

      expect(result.rows).toHaveLength(2);
    });
  });

  describe('findByCustomerId', () => {
    beforeEach(async () => {
      // Seed test data
      await orderRepository.save({
        id: 'order-1',
        customerId: 'customer-A',
        items: [],
        total: 100,
        status: 'COMPLETED'
      });
      await orderRepository.save({
        id: 'order-2',
        customerId: 'customer-A',
        items: [],
        total: 200,
        status: 'PENDING'
      });
      await orderRepository.save({
        id: 'order-3',
        customerId: 'customer-B',
        items: [],
        total: 150,
        status: 'COMPLETED'
      });
    });

    it('should return orders for specific customer', async () => {
      const orders = await orderRepository.findByCustomerId('customer-A');

      expect(orders).toHaveLength(2);
      expect(orders.every(o => o.customerId === 'customer-A')).toBe(true);
    });

    it('should return empty array for customer with no orders', async () => {
      const orders = await orderRepository.findByCustomerId('customer-C');
      expect(orders).toEqual([]);
    });
  });
});

API Integration Tests

// tests/integration/OrderAPI.test.js
const request = require('supertest');
const { createApp } = require('../../app');
const { createTestDatabase, closeTestDatabase } = require('../helpers/database');

describe('Order API Integration', () => {
  let app;
  let db;
  let authToken;

  beforeAll(async () => {
    db = await createTestDatabase();
    app = createApp({ db });
    
    // Get auth token
    authToken = await getTestAuthToken();
  });

  afterAll(async () => {
    await closeTestDatabase(db);
  });

  beforeEach(async () => {
    await db.query('DELETE FROM orders');
  });

  describe('POST /orders', () => {
    it('should create order and return 201', async () => {
      const orderData = {
        items: [
          { productId: 'product-1', quantity: 2 }
        ]
      };

      const response = await request(app)
        .post('/orders')
        .set('Authorization', `Bearer ${authToken}`)
        .send(orderData)
        .expect(201);

      expect(response.body).toMatchObject({
        id: expect.any(String),
        status: 'PENDING',
        items: expect.arrayContaining([
          expect.objectContaining({ productId: 'product-1' })
        ])
      });
    });

    it('should return 400 for invalid order data', async () => {
      const response = await request(app)
        .post('/orders')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ items: [] })
        .expect(400);

      expect(response.body.error).toContain('at least one item');
    });

    it('should return 401 without auth token', async () => {
      await request(app)
        .post('/orders')
        .send({ items: [{ productId: 'p1', quantity: 1 }] })
        .expect(401);
    });
  });

  describe('GET /orders/:id', () => {
    it('should return order by id', async () => {
      // Create order first
      const createResponse = await request(app)
        .post('/orders')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ items: [{ productId: 'p1', quantity: 1 }] });

      const orderId = createResponse.body.id;

      const response = await request(app)
        .get(`/orders/${orderId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(response.body.id).toBe(orderId);
    });

    it('should return 404 for non-existent order', async () => {
      await request(app)
        .get('/orders/non-existent-id')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(404);
    });
  });
});

Contract Testing with Pact

Consumer Contract Test

// tests/contract/OrderService.consumer.test.js
const { Pact } = require('@pact-foundation/pact');
const { PaymentClient } = require('../../clients/PaymentClient');
const path = require('path');

describe('Order Service - Payment Service Contract', () => {
  const provider = new Pact({
    consumer: 'OrderService',
    provider: 'PaymentService',
    port: 8081,
    log: path.resolve(process.cwd(), 'logs', 'pact.log'),
    dir: path.resolve(process.cwd(), 'pacts'),
    logLevel: 'warn'
  });

  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());
  afterEach(() => provider.verify());

  describe('processPayment', () => {
    it('should process payment successfully', async () => {
      // Define expected interaction
      await provider.addInteraction({
        state: 'customer has valid payment method',
        uponReceiving: 'a request to process payment',
        withRequest: {
          method: 'POST',
          path: '/payments',
          headers: {
            'Content-Type': 'application/json'
          },
          body: {
            orderId: '12345',
            amount: 100.00,
            currency: 'USD',
            customerId: 'customer-123'
          }
        },
        willRespondWith: {
          status: 200,
          headers: {
            'Content-Type': 'application/json'
          },
          body: {
            id: like('pay_abc123'),
            orderId: '12345',
            status: 'COMPLETED',
            amount: 100.00,
            processedAt: like('2024-01-15T10:30:00Z')
          }
        }
      });

      // Execute the actual client
      const client = new PaymentClient(`http://localhost:8081`);
      const result = await client.processPayment({
        orderId: '12345',
        amount: 100.00,
        currency: 'USD',
        customerId: 'customer-123'
      });

      expect(result.status).toBe('COMPLETED');
      expect(result.orderId).toBe('12345');
    });

    it('should handle payment failure', async () => {
      await provider.addInteraction({
        state: 'customer has insufficient funds',
        uponReceiving: 'a request to process payment',
        withRequest: {
          method: 'POST',
          path: '/payments',
          headers: {
            'Content-Type': 'application/json'
          },
          body: {
            orderId: '12345',
            amount: 10000.00,
            currency: 'USD',
            customerId: 'customer-456'
          }
        },
        willRespondWith: {
          status: 402,
          headers: {
            'Content-Type': 'application/json'
          },
          body: {
            error: 'INSUFFICIENT_FUNDS',
            message: like('Payment declined due to insufficient funds')
          }
        }
      });

      const client = new PaymentClient(`http://localhost:8081`);
      
      await expect(
        client.processPayment({
          orderId: '12345',
          amount: 10000.00,
          currency: 'USD',
          customerId: 'customer-456'
        })
      ).rejects.toThrow('INSUFFICIENT_FUNDS');
    });
  });
});

// Pact matchers
const { like, eachLike, term } = require('@pact-foundation/pact').Matchers;

Provider Contract Test

// tests/contract/PaymentService.provider.test.js
const { Verifier } = require('@pact-foundation/pact');
const { createApp } = require('../../app');
const path = require('path');

describe('Payment Service - Provider Verification', () => {
  let server;

  beforeAll(async () => {
    const app = createApp();
    server = app.listen(8082);
  });

  afterAll(() => {
    server.close();
  });

  it('should validate the expectations of Order Service', async () => {
    const opts = {
      provider: 'PaymentService',
      providerBaseUrl: 'http://localhost:8082',
      pactUrls: [
        path.resolve(process.cwd(), 'pacts', 'orderservice-paymentservice.json')
      ],
      // Or use Pact Broker
      // pactBrokerUrl: 'https://your-broker.pactflow.io',
      // pactBrokerToken: process.env.PACT_BROKER_TOKEN,
      stateHandlers: {
        'customer has valid payment method': async () => {
          // Set up test data for valid payment method
          await seedTestData({
            customerId: 'customer-123',
            paymentMethods: [{ type: 'card', valid: true }]
          });
        },
        'customer has insufficient funds': async () => {
          // Set up test data for insufficient funds
          await seedTestData({
            customerId: 'customer-456',
            balance: 0
          });
        }
      },
      publishVerificationResult: process.env.CI === 'true',
      providerVersion: process.env.GIT_COMMIT || '1.0.0'
    };

    await new Verifier(opts).verifyProvider();
  });
});

Component Testing

// tests/component/OrderService.component.test.js
const { GenericContainer, Wait } = require('testcontainers');
const request = require('supertest');
const { createApp } = require('../../app');

describe('Order Service Component Test', () => {
  let postgresContainer;
  let kafkaContainer;
  let app;

  beforeAll(async () => {
    // Start PostgreSQL container
    postgresContainer = await new GenericContainer('postgres:15-alpine')
      .withEnvironment({
        POSTGRES_DB: 'orders_test',
        POSTGRES_USER: 'test',
        POSTGRES_PASSWORD: 'test'
      })
      .withExposedPorts(5432)
      .withWaitStrategy(Wait.forLogMessage('database system is ready'))
      .start();

    // Start Kafka container (optional)
    kafkaContainer = await new GenericContainer('confluentinc/cp-kafka:7.5.0')
      .withExposedPorts(9092)
      .start();

    // Create app with real containers
    const dbUrl = `postgresql://test:test@${postgresContainer.getHost()}:${postgresContainer.getMappedPort(5432)}/orders_test`;
    
    app = createApp({
      databaseUrl: dbUrl,
      kafkaBrokers: `${kafkaContainer.getHost()}:${kafkaContainer.getMappedPort(9092)}`
    });

    // Run migrations
    await runMigrations(dbUrl);
  }, 60000);

  afterAll(async () => {
    await postgresContainer?.stop();
    await kafkaContainer?.stop();
  });

  describe('Order Lifecycle', () => {
    it('should complete full order flow', async () => {
      // 1. Create order
      const createResponse = await request(app)
        .post('/orders')
        .set('Authorization', 'Bearer test-token')
        .send({
          customerId: 'customer-123',
          items: [
            { productId: 'product-1', quantity: 2 }
          ]
        })
        .expect(201);

      const orderId = createResponse.body.id;
      expect(createResponse.body.status).toBe('PENDING');

      // 2. Confirm order
      await request(app)
        .post(`/orders/${orderId}/confirm`)
        .set('Authorization', 'Bearer test-token')
        .expect(200);

      // 3. Check order status
      const statusResponse = await request(app)
        .get(`/orders/${orderId}`)
        .set('Authorization', 'Bearer test-token')
        .expect(200);

      expect(statusResponse.body.status).toBe('CONFIRMED');
    });
  });
});

End-to-End Testing

// tests/e2e/checkout.e2e.test.js
const axios = require('axios');

describe('Checkout E2E Flow', () => {
  const baseUrl = process.env.E2E_BASE_URL || 'http://localhost:8080';
  let authToken;
  let userId;

  beforeAll(async () => {
    // Login and get auth token
    const loginResponse = await axios.post(`${baseUrl}/auth/login`, {
      email: '[email protected]',
      password: 'test-password'
    });
    
    authToken = loginResponse.data.accessToken;
    userId = loginResponse.data.userId;
  });

  const api = () => axios.create({
    baseURL: baseUrl,
    headers: { Authorization: `Bearer ${authToken}` }
  });

  describe('Complete Checkout Journey', () => {
    let orderId;

    it('should add items to cart', async () => {
      const response = await api().post('/cart/items', {
        productId: 'product-123',
        quantity: 2
      });

      expect(response.status).toBe(200);
      expect(response.data.items).toHaveLength(1);
    });

    it('should create order from cart', async () => {
      const response = await api().post('/orders', {
        shippingAddress: {
          street: '123 Test St',
          city: 'Test City',
          country: 'US',
          postalCode: '12345'
        }
      });

      expect(response.status).toBe(201);
      orderId = response.data.id;
      expect(response.data.status).toBe('PENDING');
    });

    it('should process payment', async () => {
      const response = await api().post(`/orders/${orderId}/pay`, {
        paymentMethod: {
          type: 'card',
          token: 'tok_visa'
        }
      });

      expect(response.status).toBe(200);
      expect(response.data.paymentStatus).toBe('COMPLETED');
    });

    it('should show order as confirmed', async () => {
      // Wait for async processing
      await new Promise(resolve => setTimeout(resolve, 2000));

      const response = await api().get(`/orders/${orderId}`);

      expect(response.status).toBe(200);
      expect(response.data.status).toBe('CONFIRMED');
      expect(response.data.paymentId).toBeDefined();
    });

    it('should update inventory', async () => {
      const response = await api().get('/inventory/product-123');

      expect(response.status).toBe(200);
      // Verify inventory was reduced
    });
  });
});

Test Environment Setup

# docker-compose.test.yml
version: '3.8'

services:
  test-db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: test
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    ports:
      - "5433:5432"
    tmpfs:
      - /var/lib/postgresql/data  # Speed up tests

  test-redis:
    image: redis:7-alpine
    ports:
      - "6380:6379"

  test-kafka:
    image: confluentinc/cp-kafka:7.5.0
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9093
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@test-kafka:9093
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      CLUSTER_ID: 'test-cluster-id'
    ports:
      - "9093:9092"

Jest Configuration

// jest.config.js
module.exports = {
  projects: [
    {
      displayName: 'unit',
      testMatch: ['<rootDir>/tests/unit/**/*.test.js'],
      testEnvironment: 'node',
      setupFilesAfterEnv: ['<rootDir>/tests/setup/unit.js']
    },
    {
      displayName: 'integration',
      testMatch: ['<rootDir>/tests/integration/**/*.test.js'],
      testEnvironment: 'node',
      setupFilesAfterEnv: ['<rootDir>/tests/setup/integration.js'],
      globalSetup: '<rootDir>/tests/setup/integration-global-setup.js',
      globalTeardown: '<rootDir>/tests/setup/integration-global-teardown.js'
    },
    {
      displayName: 'e2e',
      testMatch: ['<rootDir>/tests/e2e/**/*.test.js'],
      testEnvironment: 'node',
      testTimeout: 30000
    }
  ],
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/**/*.test.js'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

Interview Questions

Answer:Approaches:
  1. Contract Testing (Pact)
    • Consumer defines expected interactions
    • Provider verifies it can meet expectations
    • Catches breaking changes early
  2. Integration Testing
    • Test with real dependencies (test containers)
    • Slower but more realistic
    • Good for database interactions
  3. Service Virtualization
    • Mock external services
    • Consistent test data
    • Faster than real services
Best Practice:
  • Contract tests for API compatibility
  • Integration tests for data integrity
  • Mocks for unit tests
Answer:Contract Testing:
  • Consumer defines expectations (contract)
  • Provider verifies it meets contract
  • Changes detected before deployment
Why important:
  • Prevents integration failures
  • Enables independent deployment
  • Faster feedback than E2E tests
  • Documents service interactions
Consumer-Driven Contracts:
  1. Consumer writes contract tests
  2. Generates contract file
  3. Provider runs verification
  4. Breaks build if contract broken
Tools: Pact, Spring Cloud Contract
Answer:Strategies:
  1. Test Containers
    • Spin up real databases
    • Isolated per test suite
    • Clean state each run
  2. Test Data Builders
    OrderBuilder.create()
      .withCustomer('123')
      .withItems([...])
      .build();
    
  3. Database Seeding
    • Known state before tests
    • Fixtures or factories
  4. Data Cleanup
    • Truncate after tests
    • Transaction rollback
    • Isolated test databases
Best Practices:
  • Each test manages own data
  • No dependencies between tests
  • Use factories for complex objects

Summary

Key Takeaways

  • Follow the testing pyramid
  • Unit tests for business logic
  • Contract tests for API compatibility
  • Integration tests with test containers
  • E2E for critical paths only

Next Steps

In the next chapter, we’ll cover Interview Preparation - common questions and system design exercises.