Back to Blog
TestingMicroservicesIntegration TestingContract Testing

Testing Microservices: Strategies for Distributed Systems

Test microservices effectively. Learn contract testing, integration patterns, and strategies for testing distributed systems.

B
Bootspring Team
Engineering
February 26, 2026
6 min read

Testing microservices requires different strategies than monolithic applications. This guide covers testing patterns from unit tests to production verification.

The Testing Pyramid for Microservices#

┌───────────────┐ │ E2E Tests │ Few, slow, expensive ├───────────────┤ │ Contract │ │ Tests │ Verify service contracts ├───────────────┤ │ Integration │ │ Tests │ Test with real dependencies ├───────────────┤ │ Component │ │ Tests │ Test service in isolation ├───────────────┤ │ Unit Tests │ Many, fast, cheap └───────────────┘

Unit Testing#

Test business logic in isolation:

1// order.service.test.ts 2import { OrderService } from './order.service'; 3import { mockOrderRepository, mockPaymentGateway } from '../test/mocks'; 4 5describe('OrderService', () => { 6 let service: OrderService; 7 8 beforeEach(() => { 9 service = new OrderService( 10 mockOrderRepository, 11 mockPaymentGateway 12 ); 13 }); 14 15 describe('createOrder', () => { 16 it('should create order with valid items', async () => { 17 const items = [ 18 { productId: '1', quantity: 2, price: 29.99 }, 19 ]; 20 21 const order = await service.createOrder('user-123', items); 22 23 expect(order.status).toBe('pending'); 24 expect(order.total).toBe(59.98); 25 expect(mockOrderRepository.save).toHaveBeenCalledWith( 26 expect.objectContaining({ userId: 'user-123' }) 27 ); 28 }); 29 30 it('should reject empty orders', async () => { 31 await expect(service.createOrder('user-123', [])) 32 .rejects.toThrow('Order must have at least one item'); 33 }); 34 35 it('should apply discount codes', async () => { 36 const items = [{ productId: '1', quantity: 1, price: 100 }]; 37 38 const order = await service.createOrder('user-123', items, { 39 discountCode: 'SAVE20', 40 }); 41 42 expect(order.discount).toBe(20); 43 expect(order.total).toBe(80); 44 }); 45 }); 46});

Component Testing#

Test a service with its direct dependencies:

1// order.component.test.ts 2import { Test, TestingModule } from '@nestjs/testing'; 3import { OrderController } from './order.controller'; 4import { OrderService } from './order.service'; 5import { PrismaService } from '../prisma/prisma.service'; 6import { TestDatabase } from '../test/test-database'; 7 8describe('Order Component', () => { 9 let app: TestingModule; 10 let controller: OrderController; 11 let db: TestDatabase; 12 13 beforeAll(async () => { 14 db = await TestDatabase.create(); 15 16 app = await Test.createTestingModule({ 17 controllers: [OrderController], 18 providers: [ 19 OrderService, 20 { 21 provide: PrismaService, 22 useValue: db.prisma, 23 }, 24 // Mock external services 25 { 26 provide: 'PAYMENT_SERVICE', 27 useValue: { 28 charge: jest.fn().mockResolvedValue({ id: 'payment-123' }), 29 }, 30 }, 31 ], 32 }).compile(); 33 34 controller = app.get<OrderController>(OrderController); 35 }); 36 37 afterAll(async () => { 38 await db.cleanup(); 39 }); 40 41 beforeEach(async () => { 42 await db.reset(); 43 }); 44 45 it('should create and retrieve order', async () => { 46 // Create order 47 const createResult = await controller.create({ 48 userId: 'user-1', 49 items: [{ productId: 'prod-1', quantity: 2 }], 50 }); 51 52 expect(createResult.id).toBeDefined(); 53 54 // Retrieve order 55 const order = await controller.findOne(createResult.id); 56 57 expect(order.userId).toBe('user-1'); 58 expect(order.items).toHaveLength(1); 59 }); 60});

Contract Testing with Pact#

Consumer Side#

1// order-consumer.pact.test.ts 2import { PactV3, MatchersV3 } from '@pact-foundation/pact'; 3import { ProductClient } from './product.client'; 4 5const { like, eachLike } = MatchersV3; 6 7const provider = new PactV3({ 8 consumer: 'OrderService', 9 provider: 'ProductService', 10 dir: './pacts', 11}); 12 13describe('Product Service Contract', () => { 14 it('should get product details', async () => { 15 await provider 16 .given('product 123 exists') 17 .uponReceiving('a request for product 123') 18 .withRequest({ 19 method: 'GET', 20 path: '/api/products/123', 21 }) 22 .willRespondWith({ 23 status: 200, 24 headers: { 'Content-Type': 'application/json' }, 25 body: { 26 id: like('123'), 27 name: like('Test Product'), 28 price: like(29.99), 29 inventory: like(100), 30 }, 31 }) 32 .executeTest(async (mockServer) => { 33 const client = new ProductClient(mockServer.url); 34 const product = await client.getProduct('123'); 35 36 expect(product.name).toBe('Test Product'); 37 expect(product.price).toBe(29.99); 38 }); 39 }); 40 41 it('should return 404 for non-existent product', async () => { 42 await provider 43 .given('product 999 does not exist') 44 .uponReceiving('a request for non-existent product') 45 .withRequest({ 46 method: 'GET', 47 path: '/api/products/999', 48 }) 49 .willRespondWith({ 50 status: 404, 51 body: { error: like('Product not found') }, 52 }) 53 .executeTest(async (mockServer) => { 54 const client = new ProductClient(mockServer.url); 55 56 await expect(client.getProduct('999')) 57 .rejects.toThrow('Product not found'); 58 }); 59 }); 60});

Provider Side#

1// product-provider.pact.test.ts 2import { Verifier } from '@pact-foundation/pact'; 3import { app } from '../src/app'; 4 5describe('Product Provider Verification', () => { 6 let server: any; 7 8 beforeAll(async () => { 9 server = app.listen(3001); 10 }); 11 12 afterAll(() => { 13 server.close(); 14 }); 15 16 it('should validate the expectations of OrderService', async () => { 17 const verifier = new Verifier({ 18 providerBaseUrl: 'http://localhost:3001', 19 pactUrls: ['./pacts/OrderService-ProductService.json'], 20 stateHandlers: { 21 'product 123 exists': async () => { 22 await db.products.create({ 23 id: '123', 24 name: 'Test Product', 25 price: 29.99, 26 inventory: 100, 27 }); 28 }, 29 'product 999 does not exist': async () => { 30 await db.products.deleteMany({ where: { id: '999' } }); 31 }, 32 }, 33 }); 34 35 await verifier.verifyProvider(); 36 }); 37});

Integration Testing#

Test service interactions with real dependencies:

1// order-integration.test.ts 2import { Test } from '@nestjs/testing'; 3import { INestApplication } from '@nestjs/common'; 4import request from 'supertest'; 5import { AppModule } from '../src/app.module'; 6import { setupTestContainers } from './test-containers'; 7 8describe('Order Integration Tests', () => { 9 let app: INestApplication; 10 let containers: Awaited<ReturnType<typeof setupTestContainers>>; 11 12 beforeAll(async () => { 13 // Start test containers 14 containers = await setupTestContainers(); 15 16 const moduleRef = await Test.createTestingModule({ 17 imports: [AppModule], 18 }) 19 .overrideProvider('DATABASE_URL') 20 .useValue(containers.postgresUrl) 21 .overrideProvider('REDIS_URL') 22 .useValue(containers.redisUrl) 23 .compile(); 24 25 app = moduleRef.createNestApplication(); 26 await app.init(); 27 }); 28 29 afterAll(async () => { 30 await app.close(); 31 await containers.stop(); 32 }); 33 34 describe('POST /orders', () => { 35 it('should create order and publish event', async () => { 36 const response = await request(app.getHttpServer()) 37 .post('/orders') 38 .send({ 39 userId: 'user-123', 40 items: [{ productId: 'prod-1', quantity: 2 }], 41 }) 42 .expect(201); 43 44 expect(response.body).toMatchObject({ 45 status: 'pending', 46 userId: 'user-123', 47 }); 48 49 // Verify event was published 50 const events = await containers.getPublishedEvents('orders'); 51 expect(events).toContainEqual( 52 expect.objectContaining({ 53 type: 'OrderCreated', 54 orderId: response.body.id, 55 }) 56 ); 57 }); 58 }); 59});

Using Testcontainers#

1// test-containers.ts 2import { PostgreSqlContainer } from '@testcontainers/postgresql'; 3import { RedisContainer } from '@testcontainers/redis'; 4import { KafkaContainer } from '@testcontainers/kafka'; 5 6export async function setupTestContainers() { 7 const postgres = await new PostgreSqlContainer() 8 .withDatabase('test') 9 .start(); 10 11 const redis = await new RedisContainer().start(); 12 13 const kafka = await new KafkaContainer().start(); 14 15 return { 16 postgresUrl: postgres.getConnectionUri(), 17 redisUrl: redis.getConnectionUrl(), 18 kafkaUrl: `${kafka.getHost()}:${kafka.getMappedPort(9093)}`, 19 20 async stop() { 21 await Promise.all([ 22 postgres.stop(), 23 redis.stop(), 24 kafka.stop(), 25 ]); 26 }, 27 28 async getPublishedEvents(topic: string) { 29 // Read from Kafka topic 30 }, 31 }; 32}

End-to-End Testing#

1// e2e/checkout-flow.test.ts 2import { test, expect } from '@playwright/test'; 3 4test.describe('Checkout Flow', () => { 5 test('should complete purchase', async ({ page }) => { 6 // Add item to cart 7 await page.goto('/products/123'); 8 await page.click('button:has-text("Add to Cart")'); 9 10 // Go to checkout 11 await page.click('a:has-text("Cart")'); 12 await page.click('button:has-text("Checkout")'); 13 14 // Fill shipping info 15 await page.fill('[name="address"]', '123 Main St'); 16 await page.fill('[name="city"]', 'New York'); 17 await page.fill('[name="zip"]', '10001'); 18 19 // Submit order 20 await page.click('button:has-text("Place Order")'); 21 22 // Verify confirmation 23 await expect(page.locator('h1')).toHaveText('Order Confirmed'); 24 await expect(page.locator('.order-id')).toBeVisible(); 25 }); 26});

Testing Strategies#

Chaos Testing#

1// chaos.test.ts 2describe('Resilience Tests', () => { 3 it('should handle database timeout gracefully', async () => { 4 // Inject latency 5 await toxiproxy.addToxic('postgres', { 6 type: 'latency', 7 attributes: { latency: 5000 }, 8 }); 9 10 const response = await request(app) 11 .get('/api/orders/123') 12 .timeout(3000); 13 14 expect(response.status).toBe(503); 15 expect(response.body.error).toBe('Service temporarily unavailable'); 16 }); 17 18 it('should use circuit breaker on repeated failures', async () => { 19 // Simulate service down 20 await mockServer.stubFor( 21 get('/products/123').willReturn( 22 aResponse().withStatus(500) 23 ) 24 ); 25 26 // First 5 requests fail 27 for (let i = 0; i < 5; i++) { 28 await request(app).post('/orders').send(orderData); 29 } 30 31 // Circuit should be open now - fast fail 32 const start = Date.now(); 33 const response = await request(app).post('/orders').send(orderData); 34 const duration = Date.now() - start; 35 36 expect(duration).toBeLessThan(100); // Fast fail 37 expect(response.body.error).toBe('Service unavailable'); 38 }); 39});

Best Practices#

  1. Test at the right level: Most tests should be unit/component tests
  2. Use contract tests: Catch breaking changes early
  3. Test failure modes: Services will fail, test handling
  4. Parallelize tests: Use containers for isolation
  5. Mock external services: Don't depend on third parties in CI

Conclusion#

Testing microservices requires a layered approach. Start with unit tests for business logic, use contract tests for service boundaries, and reserve E2E tests for critical user journeys.

Share this article

Help spread the word about Bootspring