PHPUnitでよく使うMockeryまとめ

# 背景

業務でモックを使用する機会があったのと、以前Mockeryを使用する際に苦戦したこともあって、業務で使用するMockeryの記法をまとめようと思いました。

# モックとは

モックとは、ある機能に似せたメソッドなどを簡易的に用意できるものです。中身を実装していない状態でも見かけだけ似せたものを簡単に作成できます。

※mock up = 実物に似せた模型という意味

PHPで使用できるモックにはMockeryというライブラリがあります。

## モックを利用できる場面

「メソッドAの中で、メソッドB、Cを使用している。しかし、メソッドBを実装していない段階でメソッドAのテストを行いたい」といった場面で使えます。

この例でメソッドBのモックを用意していない場合は、メソッドAのUnitテストを行った際にメソッドBが用意されていないことでエラーが発生してしまいます。

```php
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;
    }
}
```

```php
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](https://www.wantedly.com/companies/nightley/post_articles/470944)

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

```
スタブ: 指定された挙動をする機能
スパイ: （スタブの機能） + 記録機能
モック: （スタブの機能） + 処理中の検証機能
```
[ref](https://gotohayato.com/content/483/#:~:text=%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E6%A9%9F%E8%83%BD%E3%81%AF,%E3%81%A6%E3%81%84%E3%82%8B%E3%81%93%E3%81%A8%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82)

# Mockery

## 基本的な使い方

```php
Mockery::mock(A::class) // モックしたいクラス
    ->once()  // 一回だけ呼び出される
    ->shouldReceive('b')  // モックしたいメソッド
    ->with(1) // 引数のモック
    ->andReturn('2の倍数');  // 返却値をモック
```

## with

どんな引数でもいい場合は、\Mockery::any()を渡す。

```php
Mockery::mock(A::class)
    ->shouldReceive('b')
    ->with(Mockery::any());
```

引数の検証にはMockery::on()を使用する。

```php
Mockery::mock(A::class)
    ->shouldReceive('b')
    ->with(Mockery::on(function ($argument) {
        // $argumentは引数、引数が2の倍数かどうか
        if ($argument % 2 == 0) {
            return true; // エクスペクションと一致
        }
        return false; // NoMatchingExpectationException
    }));
```

## staticメソッドのモック

```php
Mockery::mock('alias:' . A::class)
    ->shouldReceive('d');
```

## エラーをスロー

```php
Mockery::mock(A::class)
    ->once()
    ->shouldReceive('b')
    ->with(1)
    ->andThrows(Exception::class);
```

## モックのクリア

```php
Mockery::close();
```

Mockeryのオブジェクトをクリアし、エクスペクションのために必要な検査タスクが実行される。

# PHPUnit内でよく使用するMockery

## Laravelコマンドでエラーを発生させる

コマンド

```php
class TestCommand
{
    ...
    public function handle()
    {
        try {
            // 実装
            (new A())->b();
        } catch (Throwable $e) {
            return Command::FAILURE;
        }
        return Command::SUCCESS;
    }
    ...
}
```

テスト

```php
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メソッドで各テストメソッドで使用するメソッドをモックしてしまうと、モックしたくないテストメソッドでもモックされてしまいます。

```php
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するか、オブジェクトを削除するまでモックが持続されます。

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

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

# 最後に

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