背景
業務でモックを使用する機会があったのと、以前Mockeryを使用する際に苦戦したこともあって、業務で使用するMockeryの記法をまとめようと思いました。
モックとは
モックとは、ある機能に似せたメソッドなどを簡易的に用意できるものです。中身を実装していない状態でも見かけだけ似せたものを簡単に作成できます。
※mock up = 実物に似せた模型という意味
PHPで使用できるモックにはMockeryというライブラリがあります。
モックを利用できる場面
「メソッドAの中で、メソッドB、Cを使用している。しかし、メソッドBを実装していない段階でメソッドAのテストを行いたい」といった場面で使えます。
この例でメソッドBのモックを用意していない場合は、メソッドAのUnitテストを行った際にメソッドBが用意されていないことでエラーが発生してしまいます。
class A
{
public function a(int $number)
{
if ($this->b($number) && $this->c($number)) {
return '15の倍数';
}
}
public function b(int $number)
{
// 3の倍数かどうか
}
public function c(int)
{
// 5の倍数かどうか
if ($number % 5 === 0) {
return true;
}
return false;
}
}
class ATest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
}
/**
* @test
*/
public function 15の倍数であることが表示される()
{
$message = (new A())->a(); // ERROR
$this->assertSame('15の倍数', $message);
}
}
モック、スタブ、スパイ
Mockeryを使用したモックは、正確にはテストダブルと呼ばれます、テスト対象が依存しているコンポーネントを置き換える代用品を意味します。ref
テストダブルには、モックの他にスタブ、スパイが存在しますが、正直違いがよく分かりません(他にも
フェイクやダミーなどあるらしい)。個人的にはテストダブルの概念だけ捉えておけば問題ないと思っているのですが、参考になる記述があったので引用させて頂きます。
スタブ: 指定された挙動をする機能
スパイ: (スタブの機能) + 記録機能
モック: (スタブの機能) + 処理中の検証機能
Mockery
基本的な使い方
Mockery::mock(A::class) // モックしたいクラス
->once() // 一回だけ呼び出される
->shouldReceive('b') // モックしたいメソッド
->with(1) // 引数のモック
->andReturn('2の倍数'); // 返却値をモック
with
どんな引数でもいい場合は、\Mockery::any()を渡す。
Mockery::mock(A::class)
->shouldReceive('b')
->with(Mockery::any());
引数の検証にはMockery::on()を使用する。
Mockery::mock(A::class)
->shouldReceive('b')
->with(Mockery::on(function ($argument) {
// $argumentは引数、引数が2の倍数かどうか
if ($argument % 2 == 0) {
return true; // エクスペクションと一致
}
return false; // NoMatchingExpectationException
}));
staticメソッドのモック
Mockery::mock('alias:' . A::class)
->shouldReceive('d');
エラーをスロー
Mockery::mock(A::class)
->once()
->shouldReceive('b')
->with(1)
->andThrows(Exception::class);
モックのクリア
Mockery::close();
Mockeryのオブジェクトをクリアし、エクスペクションのために必要な検査タスクが実行される。
PHPUnit内でよく使用するMockery
Laravelコマンドでエラーを発生させる
コマンド
class TestCommand
{
...
public function handle()
{
try {
// 実装
(new A())->b();
} catch (Throwable $e) {
return Command::FAILURE;
}
return Command::SUCCESS;
}
...
}
テスト
class TestCommandTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
}
/**
* @test
*/
public function コマンドが失敗する()
{
Mockery::mock(A::class)
->shouldReceive('b')
->andReturn(Exception::class);
$this->artisan('command:TestCommand')->assertExitCode(Command::FAILURE)
}
}
PHPUnit内でMockeryを使用する際の注意点
PHPUnitのテストにはライフサイクルがあります。setUp()メソッドから始まり、テストメソッド、tearDown()メソッドと実行していきます。
setUp()とtearDown()は各テストメソッド共通ですから、例えばsetUpメソッドで各テストメソッドで使用するメソッドをモックしてしまうと、モックしたくないテストメソッドでもモックされてしまいます。
class TestCommandTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Mockery::mock(A::class)
->once()
->shouldReceive('b')
->with(1)
->andThrows(Exception::class);
}
/**
* @test
*/
public function 15の倍数であることが表示されない()
{
$this->expectException(Exception::class);
$message = (new A())->a();
}
/**
* @test
*/
public function 15の倍数であることが表示される()
{
$message = (new A())->a(); // モックされたままなのでERROR
$this->assertSame('15の倍数', $message);
}
}
また、モックされた後はcloseするか、オブジェクトを削除するまでモックが持続されます。
class TestCommandTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->mock = Mockery::mock(A::class)
->once()
->shouldReceive('b')
->with(1)
->andThrows(Exception::class);
}
/**
* @test
*/
public function 15の倍数であることが表示されない()
{
$this->expectException(Exception::class);
$message = (new A())->a();
}
/**
* @test
*/
public function 15の倍数であることが表示される()
{
unset($this->mock);
$message = (new A())->a(); // モックオブジェクトを削除しているのでOK
$this->assertSame('15の倍数', $message);
}
}
モックをどこで行うか、注意が必要です。
最後に
この記事が誰かの役に立てたら幸いですー。