【ITニュース解説】Module 4: Uncovering Test Doubles (Mocks and Stubs)
2025年09月07日に「Dev.to」が公開したITニュース「Module 4: Uncovering Test Doubles (Mocks and Stubs)」について初心者にもわかりやすいように丁寧に解説しています。
ITニュース概要
システム開発で連携するクラスの単体テスト時、依存部分を「テストダブル」で代替する。これは、テスト対象を隔離し、動作を制御し、テストを高速化する技術だ。特に、返り値を模倣する「スタブ」と、メソッド呼び出しを検証する「モック」があり、これらを理解することが効果的な単体テストには不可欠である。
ITニュース解説
ソフトウェア開発では、複数のプログラム部品(クラス)が連携し合って一つの機能を実現する場面が多く存在する。例えば、ユーザー登録を処理するUserServiceクラスが、登録完了後にメール送信を行うEmailClientクラスや、活動ログを記録するLoggerクラスと協力して動作するようなケースが一般的だ。このような相互に依存するクラス群の中で、UserService自身のロジックが正しく動作するかを検証したいとき、毎回実際にメールを送信したり、物理的なログファイルに書き込んだりすることは現実的ではない。それは、テストの実行に時間がかかったり、実際の外部サービスに不要な負荷をかけたり、テスト環境の準備が複雑になったりするなどの問題を引き起こすためである。
この問題を解決するために、「テストダブル」という概念が用いられる。テストダブルとは、テストの目的のために、実際のオブジェクトの代わりをする偽物のオブジェクトの総称である。これは、テスト対象のユニット(Unit Under Test; UUT)が依存している他のオブジェクトの代役を務めることで、テスト対象のユニットを外部の依存関係から切り離し、独立した状態でテストすることを可能にする。テストダブルを利用する主な目的は三つある。一つは、テスト対象のユニットを隔離すること。これにより、テストが失敗した場合に、その原因がテスト対象のコードそのものにあるのか、それとも依存している外部コードにあるのかを明確に区別しやすくなる。二つ目は、テスト環境を制御すること。例えば、データベース接続の失敗やネットワークのタイムアウトなど、実際の環境で再現が難しい特定の状況を意図的に作り出し、テスト対象のコードがそうした状況にどう対応するかを検証できる。三つ目は、テストの実行速度を向上させること。ネットワーク通信やデータベースアクセスといった時間のかかる処理を、メモリ上でのシンプルな動作に置き換えることで、テストの実行が非常に高速になる。PHPUnitなどのテストフレームワークでは、特にスタブとモックという二種類のテストダブルが頻繁に用いられる。
スタブは、テスト中に呼び出されるメソッドに対して「事前に決められた固定の返答」を提供するテストダブルである。その主な役割は、テスト対象のユニットが必要とするデータを受け取れるようにし、テストの実行を円滑に進めることにある。スタブは、依存オブジェクトが特定の結果を返すことを前提として、テスト対象のロジックを検証したい場合に有効だ。
具体的な例として、挨拶メッセージを生成するWelcomeGeneratorクラスが、挨拶文を提供するTranslatorインターフェースに依存しているケースを考える。
1// src/Translator.php 2interface Translator 3{ 4 public function getGreeting(): string; 5} 6 7// src/WelcomeGenerator.php 8class WelcomeGenerator 9{ 10 private $translator; 11 12 public function __construct(Translator $translator) 13 { 14 $this->translator = $translator; 15 } 16 17 public function greet(string $name): string 18 { 19 $greeting = $this->translator->getGreeting(); 20 return "{$greeting}, {$name}!"; 21 } 22}
WelcomeGeneratorをテストする際、実際のTranslatorの実装に頼る必要はない。代わりに、スタブを使ってTranslatorが特定の挨拶文を返すように設定する。
1// tests/WelcomeGeneratorTest.php 2use PHPUnit\Framework\TestCase; 3 4class WelcomeGeneratorTest extends TestCase 5{ 6 public function testGreetingInEnglish() 7 { 8 // 1. Translatorインターフェースのスタブを作成 9 $translatorStub = $this->createStub(Translator::class); 10 11 // 2. スタブを設定: 'getGreeting'メソッドが呼び出されたら'Hello'を返す 12 $translatorStub->method('getGreeting')->willReturn('Hello'); 13 14 // 3. テスト対象のクラスにスタブを注入 15 $generator = new WelcomeGenerator($translatorStub); 16 17 // 4. メソッドを実行し、結果を検証 18 $result = $generator->greet('John'); 19 $this->assertEquals('Hello, John!', $result); 20 } 21}
このテストでは、Translatorの内部動作は関係なく、getGreetingメソッドが「Hello」を返すことだけが重要である。これにより、WelcomeGeneratorがその挨拶文と名前を正しく結合するかを検証できる。createStub()はスタブオブジェクトを生成し、method('メソッド名')->willReturn('戻り値')で、呼び出された際に指定した値を返すように設定する。
一方、モックはスタブのように値を返す機能も持つが、その主要な目的は、依存オブジェクトの「メソッドが特定の引数で、何回呼び出されたか」といった、オブジェクト間の相互作用を検証することにある。モックは、テスト対象のクラスが依存オブジェクトのメソッドを正しく呼び出していることを確認したい場合に用いる。
例として、ユーザー登録を行うUserRegistrarクラスが、登録後にLoggerインターフェースのlogメソッドを呼び出して活動を記録するケースを考える。
1// src/Logger.php 2interface Logger 3{ 4 public function log(string $message): void; 5} 6 7// src/UserRegistrar.php 8class UserRegistrar 9{ 10 private $logger; 11 12 public function __construct(Logger $logger) 13 { 14 $this->logger = $logger; 15 } 16 17 public function register(string $name) 18 { 19 // ユーザーを保存するロジック... 20 21 // ロガーに通知 22 $this->logger->log("User {$name} registered successfully."); 23 } 24}
UserRegistrarのテストでは、実際にログを記録するのではなく、registerメソッドが実行された際にLoggerのlogメソッドが正しく呼び出されたことを確認したい。
1// tests/UserRegistrarTest.php 2use PHPUnit\Framework\TestCase; 3 4class UserRegistrarTest extends TestCase 5{ 6 public function testShouldLogMessageOnRegister() 7 { 8 // 1. Loggerインターフェースのモックを作成 9 $loggerMock = $this->createMock(Logger::class); 10 11 // 2. 期待値を設定: 'log'メソッドが正確に1回呼び出されることを期待 12 // かつ、その引数が'User Alice registered successfully.'であることを期待 13 $loggerMock->expects($this->once()) 14 ->method('log') 15 ->with($this->equalTo('User Alice registered successfully.')); 16 17 // 3. テスト対象のクラスにモックを注入 18 $registrar = new UserRegistrar($loggerMock); 19 20 // 4. メソッドを実行 21 $registrar->register('Alice'); 22 } 23}
モックでは、createMock()でモックオブジェクトを生成し、expects($this->once())で、特定のメソッドがテスト中に何回呼び出されるべきかという「期待」を定義する。$this->once()は「1回だけ」を意味し、他にもany()(任意の回数)、never()(一度も呼び出されない)などが存在する。method('メソッド名')で監視するメソッドを指定し、with(...)で呼び出し時の引数を指定できる。このexpectsによる期待値設定自体がアサーションの役割を果たすため、テストコードの最後に明示的な検証がなくても、期待が満たされない場合はテストが失敗する。
スタブとモックはどちらもテストダブルだが、その用途と焦点には重要な違いがある。スタブの主な目的は、テスト対象のユニットに特定の状態(データ)を提供し、そのユニット自身のロジックが正しく機能するかを検証することだ。テストの焦点は、テスト対象ユニットの最終的な状態や出力にある。一方、モックの主な目的は、テスト対象のユニットが依存オブジェクトとどのように相互作用するか、つまり、特定のメソッドが正しいタイミングで、正しい引数で呼び出されたかという振る舞いを検証することにある。テストの焦点は、テスト対象オブジェクトとその依存オブジェクト間のコミュニケーションだ。スタブではテスト対象のコードの結果に対してアサーションを行うが、モックではモック自身が期待通りに利用されたか(expects句で定義された内容)を検証する。
スタブやモックの他にもテストダブルは存在する。フェイク(Fakes)は、本番環境のオブジェクトと同じインターフェースを持つが、実装がより簡素化されたオブジェクトである。例えば、実際のデータベースの代わりにメモリ上で動作する簡易的なデータベースなどがこれに当たる。スパイ(Spies)は、実際のオブジェクトのメソッド呼び出しを監視するテストダブルで、メソッド呼び出しを妨げずに、後からどのような呼び出しが行われたかを検証するために利用される。PHPUnitでは、柔軟な期待値設定を持つモックを用いることで、スパイと同様の振る舞いを実現できる場合が多い。
テストダブル、特にスタブとモックを理解し適切に使いこなすことは、効果的なユニットテストを記述する上で非常に重要である。これにより、複雑な依存関係を持つシステムでも、個々の部品を独立させて信頼性の高いテストを構築できるようになる。この技術の習得は、システムエンジニアとして高品質なソフトウェアを開発するための不可欠なスキルとなるだろう。