Mockeryによるモックとスタブ
柔軟なテストダブルを使ってユニットを分離します。
「Mockeryによるモックとスタブ」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
専用のモックライブラリを使う理由
PHPUnitにも独自のテストダブルAPIがありますが、Mockeryはより流暢で表現力の高い構文を提供し、PHPUnitのモックにはない機能も備えています。部分モック、スパイ、柔軟な引数マッチング、呼び出し順序の期待値などです。協調関係の多い複雑なコードでは、Mockeryのほうが読みやすいことがよくあります。このレッスンでは、さまざまなテストダブルと、それぞれを使うタイミングを扱います。
composer require --dev mockery/mockeryStubとMockとSpyの違い
正確な用語を使うと、テストの混乱を防げます。
- Stub — あらかじめ決めた値を返します。状態、つまり結果をアサートします。
- Mock — 呼び出しに対する期待値を持ちます。振る舞い、つまり正しく呼び出されたことをアサートします。期待どおりでなければテストは失敗します。
- Spy — 呼び出しを記録し、後からアサートできるようにします。
目安として、クエリにはStub、コマンドにはMockまたはSpyを優先してください。
allows()を使ったStub
Mockery::mock()とallows()(またはshouldReceive()->andReturn())を使うと、呼び出しが発生したことをアサートせずに、あらかじめ決めた戻り値を提供できます。ここではゲートウェイのStubに既知の為替レートを返させ、計算を分離してテストします。
<?php
use Mockery;
use PHPUnit\Framework\TestCase;
final class ConverterTest extends TestCase
{
public function test_converts_using_rate(): void
{
$rates = Mockery::mock(RateGateway::class);
$rates->allows()->rateFor('USD', 'EUR')->andReturn(0.9);
$sut = new Converter($rates);
self::assertSame(90.0, $sut->convert(100.0, 'USD', 'EUR'));
Mockery::close();
}
}
expects()を使ったMock
操作そのものが契約となる場合、たとえばメールを正確に1回必ず送信しなければならない場合は、expects()またはshouldReceive()->once()を使います。MockeryはMockery::close()の実行時に期待値を検証し、期待された呼び出しがなければテストを失敗させます。
<?php
use Mockery;
$mailer = Mockery::mock(Mailer::class);
$mailer->expects()
->send(Mockery::type(Message::class))
->once();
$service = new SignupService($mailer);
$service->register('ada@example.com');
Mockery::close(); // fails here if send() was never called
引数マッチャー
Mockeryのマッチャーを使うと、期待値を必要に応じて緩くしたり厳密にしたりできます。
Mockery::any()— 任意の値です。Mockery::type('string')/クラス名 — 型を検査します。Mockery::on(fn($a) => ...)— カスタム述語です。Mockery::capture($var)— 後でアサートできるように引数を取得します。
<?php
use Mockery;
$repo = Mockery::mock(UserRepo::class);
$repo->expects()
->save(Mockery::on(fn(User $u) => $u->isActive()))
->once()
->andReturnTrue();
戻り値のシーケンスと動的な戻り値
呼び出しごとに複数の戻り値を順番に設定したり、andReturnUsing()を使って引数から戻り値を計算したりできます。これにより、リトライ処理、ページネーション、状態を持つ協調オブジェクトをモデル化できます。
<?php
use Mockery;
$api = Mockery::mock(HttpClient::class);
// First call throws, second succeeds (retry test):
$api->shouldReceive('get')
->twice()
->andThrow(new \RuntimeException('timeout'))
->andReturn('{"ok":true}');
// Or derive the return from the input:
$api->shouldReceive('echo')
->andReturnUsing(fn(string $in) => strtoupper($in));
Spy:後からアサートする
Spyは順序を逆にします。先に実行し、その後でアサートします。Mockery::spy()が呼び出しを記録し、後からshouldHaveReceived()で確認します。事前に期待値を設定してセットアップを複雑にしたくない場合でも、Spyを使えばArrange-Act-Assertの構造をすっきり保てます。
<?php
use Mockery;
$logger = Mockery::spy(Logger::class);
$service = new PaymentService($logger);
$service->charge(500);
// Assertions happen AFTER the action:
$logger->shouldHaveReceived('info')
->with(Mockery::pattern('/charged 500/'))
->once();
Mockery::close();
部分モック
実際のオブジェクトを使いながら、1つのメソッドだけを上書きしたい場合があります。これが部分モックです。MockeryのmakePartial()は、期待値を設定したメソッド以外では実際のメソッドを呼び出します。使いすぎには注意してください。部分モックへの過度な依存は、通常、1つのクラスが多くの責務を抱えている兆候です。
<?php
use Mockery;
$report = Mockery::mock(Report::class)->makePartial();
// Only stub the slow/external method; the rest runs for real
$report->shouldReceive('fetchRawData')->andReturn(['a', 'b', 'c']);
// real summarize() runs, using the stubbed data
$summary = $report->summarize();
所有していないものをモックしない
テストにおける基本原則の1つは、サードパーティーのクラスを直接モックしないことです。サードパーティーのAPIは変更される可能性があり、モックだけが実際のAPIから気付かないうちに乖離するためです。代わりに、自分たちのインターフェースの背後にラップし、そのインターフェースをモックしてください。そうすれば、モックで自分たちの契約を検証し、アダプターの統合テストで実際の接続を検証できます。
<?php
interface PaymentGateway { // you own this
public function charge(int $cents, string $token): string;
}
final class StripeGateway implements PaymentGateway {
public function __construct(private \Stripe\StripeClient $client) {}
public function charge(int $cents, string $token): string {
return $this->client->paymentIntents->create([/* ... */])->id;
}
}
// Tests mock PaymentGateway, never \Stripe\StripeClient directly.
必ず終了し、回数を検証する
運用上、必ず行うべきことが2つあります。
Mockery::close()をtearDown()で呼び出します(またはMockeryPHPUnitIntegrationトレイトを使います)。これにより、期待値が実際に検証され、グローバルな状態もクリーンアップされます。- 明示的な回数(
once()、times(n)、never())を使います。曖昧なモックではバグを見逃してしまいます。
<?php
use PHPUnit\Framework\TestCase;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
final class OrderServiceTest extends TestCase
{
use MockeryPHPUnitIntegration; // auto-calls Mockery::close()
public function test_does_not_refund_paid_orders(): void
{
$gw = \Mockery::mock(PaymentGateway::class);
$gw->shouldReceive('refund')->never();
// ... exercise SUT ...
}
}
呼び出し順序の期待値
呼び出し順序が契約の一部になる場合があります。たとえば、commit()の前にbeginTransaction()を呼び出さなければなりません。Mockeryのordered()は順序を強制し、呼び出しの順番が違えばテストを失敗させます。本当に順序が重要な場合だけ使ってください。順序を細かく指定しすぎると、テストが壊れやすくなります。
<?php
use Mockery;
$tx = Mockery::mock(Transaction::class);
$tx->shouldReceive('begin')->once()->ordered();
$tx->shouldReceive('commit')->once()->ordered();
$service = new TransferService($tx);
$service->run();
Mockery::close(); // fails if commit() happened before begin()
確認問題
どのような場合にStubまたはMockを使いますか。
まとめ
Mockeryのテストダブルを習得しました。
- クエリにはStub(
allows)、コマンドにはMock(expects)、後からのアサートにはSpyを使います。 - 引数マッチャー(
type、on、capture)で厳密さを調整できます。 - 戻り値のシーケンスと
andReturnUsingで、状態を持つ協調オブジェクトや動的な協調オブジェクトをモデル化できます。 - 部分モックは単一のメソッドを上書きします。使用は控えめにしてください。
- 所有していないものをモックしないでください。サードパーティーは自分たちのインターフェースの背後にラップします。
- 必ず
Mockery::close()を呼び出し、明示的な呼び出し回数をアサートします。
次は統合テストと機能テストです。
よくある質問
「Mockeryによるモックとスタブ」レッスンは無料ですか?
はい。「Mockeryによるモックとスタブ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「Mockeryによるモックとスタブ」で何を学びますか?
柔軟なテストダブルを使ってユニットを分離します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「Mockeryによるモックとスタブ」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- テスト駆動開発のワークフロー
- Mockeryによるモックとスタブ
- 統合テストと機能テスト
- Infectionによるミューテーションテスト