ストーリー
テストピラミッドとアーキテクチャ
/\
/ \ E2Eテスト(少数)
/ \ → 全層を結合して検証
/──────\
/ \ 統合テスト(中程度)
/ \ → Adapter層の結合を検証
/────────────\
/ \ ユニットテスト(多数)
/ \ → Entity, Use Caseを独立検証
/──────────────────\
| テスト種別 | 対象層 | DB必要 | 速度 |
|---|---|---|---|
| ユニットテスト | Entity, Value Object | 不要 | 極めて高速 |
| Use Caseテスト | Use Case層 | 不要(InMemory) | 高速 |
| Adapterテスト | Adapter層 | 必要(テストDB) | 中程度 |
| E2Eテスト | 全層 | 必要 | 低速 |
Layer 1: Entityのテスト
Entityは純粋なビジネスロジックなので、外部依存なしでテストできます。
describe('Order', () => {
it('注文の合計金額を計算できる', () => {
const order = Order.create('customer-1', [
OrderItem.create('prod-1', 'TypeScript入門', 2, Money.of(3000, 'JPY')),
OrderItem.create('prod-2', 'React実践', 1, Money.of(4500, 'JPY')),
]);
expect(order.totalAmount.equals(Money.of(10500, 'JPY'))).toBe(true);
});
it('PENDING以外のステータスからはキャンセルできない', () => {
const order = Order.create('customer-1', [
OrderItem.create('prod-1', '商品', 1, Money.of(1000, 'JPY')),
]);
order.confirm();
order.ship();
expect(() => order.cancel()).toThrow('この注文はキャンセルできません');
});
});
describe('Money', () => {
it('異なる通貨の加算はエラー', () => {
const jpy = Money.of(1000, 'JPY');
const usd = Money.of(10, 'USD');
expect(() => jpy.add(usd)).toThrow('異なる通貨の加算はできません');
});
it('負の金額は作成できない', () => {
expect(() => Money.of(-100, 'JPY')).toThrow('金額は0以上');
});
});
Layer 2: Use Caseのテスト
テストダブル(Stub, Spy, InMemory)を使って全依存を差し替えます。
// テストダブルの定義
class StubPaymentGateway implements PaymentGateway {
private shouldSucceed = true;
setSuccess(value: boolean): void { this.shouldSucceed = value; }
async charge(amount: Money, method: PaymentMethod): Promise<PaymentResult> {
return this.shouldSucceed
? PaymentResult.success('pay_test_123', amount)
: PaymentResult.failure('決済エラー');
}
async refund(paymentId: string, amount: Money): Promise<RefundResult> {
return RefundResult.of('ref_test_123', 'succeeded');
}
}
class SpyNotificationSender implements NotificationSender {
private calls: Array<{ order: Order }> = [];
async sendOrderConfirmation(order: Order): Promise<void> {
this.calls.push({ order });
}
async sendShippingNotification(): Promise<void> {}
wasCalled(): boolean { return this.calls.length > 0; }
callCount(): number { return this.calls.length; }
}
// Use Caseのテスト
describe('CreateOrderUseCase', () => {
let useCase: CreateOrderUseCase;
let orderRepo: InMemoryOrderRepository;
let paymentGw: StubPaymentGateway;
let notifier: SpyNotificationSender;
beforeEach(() => {
orderRepo = new InMemoryOrderRepository();
paymentGw = new StubPaymentGateway();
notifier = new SpyNotificationSender();
const inventoryChecker = new StubInventoryChecker(true);
useCase = new CreateOrderUseCase(
orderRepo, inventoryChecker, paymentGw, notifier
);
});
it('注文作成後に通知が送られる', async () => {
paymentGw.setSuccess(true);
await useCase.execute({
customerId: 'c-1',
items: [{ productId: 'p-1', quantity: 1 }],
paymentMethod: { type: 'credit_card', token: 'tok' },
shippingAddress: '東京都',
});
expect(notifier.wasCalled()).toBe(true);
expect(notifier.callCount()).toBe(1);
});
it('決済失敗時は注文が保存されない', async () => {
paymentGw.setSuccess(false);
await expect(useCase.execute({
customerId: 'c-1',
items: [{ productId: 'p-1', quantity: 1 }],
paymentMethod: { type: 'credit_card', token: 'tok' },
shippingAddress: '東京都',
})).rejects.toThrow('決済に失敗しました');
expect(orderRepo.size()).toBe(0);
});
});
Layer 3: Adapterのテスト
Adapterのテストでは、実際のインフラストラクチャ(テストDB等)を使います。
// Prisma Repositoryの統合テスト
describe('PrismaOrderRepository', () => {
let repo: PrismaOrderRepository;
let prisma: PrismaClient;
beforeAll(async () => {
prisma = new PrismaClient({
datasources: { db: { url: process.env.TEST_DATABASE_URL } },
});
repo = new PrismaOrderRepository(prisma);
});
afterEach(async () => {
await prisma.order.deleteMany();
});
afterAll(async () => {
await prisma.$disconnect();
});
it('注文を保存して取得できる', async () => {
const order = Order.create('customer-1', [
OrderItem.create('p-1', '商品A', 2, Money.of(1000, 'JPY')),
]);
await repo.save(order);
const found = await repo.findById(order.id);
expect(found).not.toBeNull();
expect(found!.id.equals(order.id)).toBe(true);
expect(found!.totalAmount.amount).toBe(2000);
});
});
テスタビリティのチェックリスト
| 確認項目 | OK条件 |
|---|---|
| Entityのテスト | 外部依存なしで実行可能 |
| Use Caseのテスト | InMemory/Stubで実行可能 |
| テスト実行時間 | ユニットテストは1秒以内 |
| テストの独立性 | テスト間で状態が共有されない |
| カバレッジ | ビジネスロジックのカバレッジ80%以上 |
まとめ
| ポイント | 内容 |
|---|---|
| テストピラミッド | ユニット(多) → 統合(中) → E2E(少) |
| Entityテスト | 純粋なビジネスロジック、外部依存なし |
| Use Caseテスト | Stub/Spy/InMemoryで全依存を差し替え |
| Adapterテスト | テストDB等の実インフラを使用 |
| 設計の指標 | テストしにくい = アーキテクチャに問題がある |
チェックリスト
- テストピラミッドと各テスト種別の対応を理解した
- Stub、Spy、InMemoryの使い分けを説明できる
- 各層のテスト方法を理解した
- テスタビリティがアーキテクチャの品質指標であることを理解した
次のステップへ
次は演習です。クリーンアーキテクチャの4層構造でアプリケーションを再構築し、各層のテストを書いてみましょう。
推定読了時間: 25分