Chuyển đến nội dung chính

レッスン 20: マイクロサービスのテスト — ユニット、統合、E2E

マイクロサービスのピラミッドをテストしています。モック/スタブを使用した単体テスト。 Testcontainer を使用した統合テスト。 E2E テスト戦略。サービスの仮想化。本番環境でのテスト (カナリア、機能フラグ)。

🏗️ アーキテクチャ — レッスン 20 レッスン 20: マイクロサービスのテスト — 単元 統合とE2E

マイクロサービスとマイクロ フロントエンドのシステム設計 — 基本から運用まで

パート 7: フルスタック マイクロサービスとマイクロ フロントエンドのテスト

xdev.asia

はじめに

マイクロサービスのテストは、依存関係が分散されているため、モノリスよりも困難です。この記事では、個別の単体テストからサービス全体にわたる E2E テストまで、包括的なテスト戦略を構築します。


1. マイクロサービス用のピラミッドのテスト

                    ┌─────┐
                    │ E2E │  Ít test, chậm, expensive
                    │Tests│  (5-10 critical flows)
                   ┌┴─────┴┐
                   │Contract│  Verify API contracts
                   │ Tests  │  between services
                  ┌┴───────┴┐
                  │Integration│  Service + DB + deps
                  │  Tests   │  (Testcontainers)
                 ┌┴─────────┴┐
                 │  Unit Tests │  Fast, isolated
                 │  (70-80%)   │  Business logic
                 └─────────────┘

2. 単体テスト

ビジネス ロジックをデータベース、ネットワーク、外部サービスから 分離してテストします。

// Order Service - business logic
describe('OrderService', () => {
  it('should calculate total with discount', () => {
    const items = [
      { productId: '1', price: 100, quantity: 2 },
      { productId: '2', price: 50, quantity: 1 },
    ];
    const discount = { type: 'percentage', value: 10 };
    
    const total = calculateOrderTotal(items, discount);
    
    expect(total).toBe(225); // (200 + 50) * 0.9
  });
  
  it('should reject order with empty items', () => {
    expect(() => createOrder([])).toThrow('Order must have items');
  });
});

外部サービスをモックする

// Mock Product Service call
const mockProductService = {
  getProduct: jest.fn().mockResolvedValue({
    id: '1', name: 'Laptop', price: 999, inStock: true
  }),
};

describe('OrderService.placeOrder', () => {
  it('should validate product exists', async () => {
    const order = await placeOrder(
      { productId: '1', qty: 1 },
      { productService: mockProductService }
    );
    expect(mockProductService.getProduct).toHaveBeenCalledWith('1');
    expect(order.status).toBe('PENDING');
  });
});

3. 統合テスト (テストコンテナ)

実際のデータベースと依存関係を使用してサービスをテストします:

// Jest + Testcontainers
import { PostgreSqlContainer } from '@testcontainers/postgresql';

describe('OrderRepository (Integration)', () => {
  let container;
  let db;
  
  beforeAll(async () => {
    container = await new PostgreSqlContainer()
      .withDatabase('test_orders')
      .start();
    db = await connectDB(container.getConnectionUri());
    await runMigrations(db);
  });
  
  afterAll(async () => {
    await container.stop();
  });
  
  it('should save and retrieve order', async () => {
    const order = await orderRepo.create({
      userId: 'user-1',
      items: [{ productId: 'p1', qty: 2, price: 100 }],
    });
    
    const found = await orderRepo.findById(order.id);
    expect(found.userId).toBe('user-1');
    expect(found.items).toHaveLength(1);
    expect(found.total).toBe(200);
  });
});

4. コンポーネントのテスト (サービスレベル)

サービス全体を分離してテストします (実際の HTTP、実際の DB、モック化された外部サービス)。

describe('Product Service (Component Test)', () => {
  let app;
  let dbContainer;
  
  beforeAll(async () => {
    dbContainer = await new PostgreSqlContainer().start();
    app = await createApp({
      dbUrl: dbContainer.getConnectionUri(),
      inventoryService: mockInventoryService,
    });
  });
  
  it('GET /api/products returns product list', async () => {
    const res = await request(app)
      .get('/api/products')
      .expect(200);
    
    expect(res.body.data).toBeInstanceOf(Array);
  });
  
  it('POST /api/products requires auth', async () => {
    await request(app)
      .post('/api/products')
      .send({ name: 'Test' })
      .expect(401);
  });
});

5. E2E テスト (重要なフローのみ)

// E2E: Complete order flow
describe('Order Flow E2E', () => {
  it('user can browse, add to cart, and place order', async () => {
    // 1. Browse products
    const products = await api.get('/api/products');
    expect(products.status).toBe(200);
    
    // 2. Add to cart
    const cartRes = await api.post('/api/cart/items', {
      productId: products.body.data[0].id,
      quantity: 1,
    });
    expect(cartRes.status).toBe(201);
    
    // 3. Place order
    const orderRes = await api.post('/api/orders', {
      cartId: cartRes.body.cartId,
      shippingAddress: { /* ... */ },
    });
    expect(orderRes.status).toBe(201);
    expect(orderRes.body.status).toBe('PENDING');
  });
});

6. テストのベストプラクティス

練習説明
テストピラミッド70% ユニット、20% 統合、10% E2E
テストコンテナ統合テスト用のモックではなく実際の DB
並列テスト各テストには独自の DB コンテナがあります。
テスト データ ビルダーテストフィクスチャの工場出荷時の機能
CI パイプラインユニット → 統合 → E2E (フェイルファスト)
機能フラグ本番環境で安全にテストする

概要

  • 単体テスト: 高速で分離されたビジネス ロジックのテスト (70 ~ 80%)
  • 統合テスト: 実際の DB テスト用のテストコンテナ
  • コンポーネント テスト: サービス全体、モック化された外部 Deps
  • E2E テスト: 重要なビジネス フローのみ (5 ~ 10 のシナリオ)
  • 契約テスト: API 契約を検証します (レッスン 22 を参照)

次の記事: レッスン 21: マイクロ フロントエンドのテスト — コンポーネント、ビジュアル、統合