はじめに
チームで単体テストの考え方/使い方を読んでいます。
当書では、単体テストの原則・実践とそのパターンが紹介され、プロジェクトの持続可能な成長を実現するための戦略について解説されています。
私自身、読む前は「単体テストの本がなんでこんなに分厚いの???」と思っていましたが、読み進めると、今までなんとなくで書いていた単体テストの理解度と解像度が上がりました。良い単体テストを書くことは良い設計につながり、結果的に持続可能な開発ができる、ということを学べる良書だと思います。
第2章「単体テストとは何か?」より
第2章「単体テストとは何か?」から単体テストにおける 古典学派 と ロンドン学派 の考え方の違いが紹介されており、以降の章でも良い単体テストを作成するための引き合いとして両派の考え方が取り上げられています。
今回は両派の考え方の違いをサンプルコードを交えて紹介します。
まずは単体テストの性質
まずは単体テストの性質ですが、
- 「単体 (unit)」と呼ばれる少量のコードを検証する
- 実行時間が短い
- 隔離された状態で実行される
3つ目の「隔離」の意見の相違が、古典学派とロンドン学派とを分けたようです。
古典学派とロンドン学派の考え方の違い
-
古典学派(デトロイト学派)
実際の依存を使用して、
全体的なシステムの振る舞いを確認するスタイル -
ロンドン学派(モック主義者)
すべての依存をモックに置き換えて、
クラス単位で細かくテストするスタイル
古典学派とロンドン学派のコードの例
古典学派とロンドン学派のそれぞれの考え方に基づいた、TypeScriptの単体テストコードのサンプルです。テストフレームワークはJestを使用しています。
テスト対象クラスとインターフェース
// IPaymentProcessor.ts - インターフェース
export interface IPaymentProcessor {
sendPayment(amount: number): boolean;
}
// PaymentProcessor.ts - 実装クラス
import { IPaymentProcessor } from './IPaymentProcessor';
export class PaymentProcessor implements IPaymentProcessor {
public sendPayment(amount: number): boolean {
// 例えば、外部API経由で送金を行う処理(実際のコードではここが外部依存に変わる可能性あり)
return true;
}
}
// OrderService.ts - テスト対象クラス
import { IPaymentProcessor } from './IPaymentProcessor';
export class OrderService {
constructor(private paymentProcessor: IPaymentProcessor) {}
public processOrder(amount: number): boolean {
if (amount > 0) {
return this.paymentProcessor.sendPayment(amount);
}
return false;
}
}
古典学派のテスト
古典学派のテストではIPaymentProcessor
は使わず、PaymentProcessor
をインスタンス化して、orderService
に渡します。
import { OrderService } from './OrderService';
import { PaymentProcessor } from './PaymentProcessor';
describe('OrderService - Classic style with actual implementation', () => {
it('should process order when amount is greater than zero', () => {
// 実際のPaymentProcessorクラスを使用してテスト
const paymentProcessor = new PaymentProcessor();
const orderService = new OrderService(paymentProcessor);
const result = orderService.processOrder(100);
// 正常に注文が処理されたかを確認
expect(result).toBe(true);
});
it('should not process order when amount is zero', () => {
const paymentProcessor = new PaymentProcessor();
const orderService = new OrderService(paymentProcessor);
const result = orderService.processOrder(0);
// 0円では処理されないはずなのでfalseを期待
expect(result).toBe(false);
});
});
ロンドン学派のテスト
ロンドン学派ではIPaymentProcessor
を用いてモックを作り、OrderService
にモックを渡しています。これにより、PaymentProcessor
からは分離できました。
言語やライブラリによって、モックの書き方はさまざまなので、少し学習コストがかかりそうな印象です。
// OrderService.spec.ts - ロンドン学派のテスト (モック化)
import { OrderService } from './OrderService';
import { IPaymentProcessor } from './IPaymentProcessor';
describe('OrderService - London style with interfaces', () => {
let mockPaymentProcessor: IPaymentProcessor;
beforeEach(() => {
mockPaymentProcessor = {
// モック化したメソッド
sendPayment: jest.fn().mockReturnValue(true)
};
});
it('should process order when amount is greater than zero', () => {
const orderService = new OrderService(mockPaymentProcessor);
const result = orderService.processOrder(100);
expect(result).toBe(true);
// 100で送金が行われた確認
expect(mockPaymentProcessor.sendPayment).toHaveBeenCalledWith(100);
});
it('should not process order when amount is zero', () => {
const orderService = new OrderService(mockPaymentProcessor);
const result = orderService.processOrder(0);
expect(result).toBe(false);
// 0のときは呼び出されないことを確認
expect(mockPaymentProcessor.sendPayment).not.toHaveBeenCalled();
});
});
まとめ
この2つの学派の違いを理解することで、すぐ単体テストが上手に書けるようになるわけではないですが、これらを学ぶことで、疎結合な設計やテストの保守性への理解が高まりました。良い単体テスト、良い設計ができるように精進したいと思います。