14
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

背景

業務でモックを使用する機会があったのと、以前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

テストダブルには、モックの他にスタブ、スパイが存在しますが、正直違いがよく分かりません(他にも
フェイクやダミーなどあるらしい)。個人的にはテストダブルの概念だけ捉えておけば問題ないと思っているのですが、参考になる記述があったので引用させて頂きます。

スタブ: 指定された挙動をする機能
スパイ: (スタブの機能) + 記録機能
モック: (スタブの機能) + 処理中の検証機能

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);
    }
}

モックをどこで行うか、注意が必要です。

最後に

この記事が誰かの役に立てたら幸いですー。

14
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?