はじめに
マイクロサービスのテストは、依存関係が分散されているため、モノリスよりも困難です。この記事では、個別の単体テストからサービス全体にわたる 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 を参照)