[PHP/Laravel] finalやreadonlyなクラスをモックする

# final (継承不可) なクラスをモックしたい！

不必要な継承を防ぐためにfinalキーワードをクラスに対してつけるような運用をしていると、時折テストで困る場合が出てくるかと思います。

例えば、finalキーワードがついているとモックができません。
かといってモックを可能にするためだけにfinalを外すのも気が引ける・・・

```
$ php artisan test

FAILED  Tests\Unit\BookingServiceTest > can send slack message  

The class \App\Services\SlackService is marked final and its methods cannot be replaced. Classes marked final can be passed in to \Mockery::mock() as instantiated objects to create a partial mock, but only if the mock is not subject to type hinting checks.
```

# dg/bypass-finalsを利用しよう

そんなときに利用できるのが`dg/bypass-finals`というパッケージです。

https://github.com/dg/bypass-finals

使い方は以下の通りです。

### composerでパッケージを追加する

```
$ composer require --dev dg/bypass-finals
.
.
Using version ^1.5 for dg/bypass-finals
```

### テスト実行時にDG/BypassFinalsを有効化する

#### オプション1. Tests\TestCaseでテスト実行前に呼ばれるように定義する

Laravelインストール時からtestsディレクトリ内に存在しているTestCaseクラスを利用します。
Laravelのテストは基本的にこのクラスを継承して作成されるため、このクラスのsetUp()にfinal / readonlyのバイパス有効化の処理を入れておけば、毎テストごとにパイパス有効化が実行され、finalやreadonlyなクラスをモックすることができます。

```php
namespace Tests;

use DG\BypassFinals;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    public function setUp(): void
    {
        parent::setUp();

        // 有効化
        BypassFinals::enable();
    }
}
```

#### オプション2. phpunit.xmlに定義

testsディレクトリにbootstrap.phpと言う名称で下記のようなファイルを作成します。
ちなみにやっていることとしては、デフォルトの設定状態でテスト開始時に呼ばれるvendor/autoload.phpを呼びつつも、プラスでDG\BypassFinals::enable()を呼び出すことでfinal / readonlyのバイパスを有効化しているのみです。

```php
<?php

require_once __DIR__ . '/../vendor/autoload.php';

// final / readonlyのバイパス有効化
DG\BypassFinals::enable();
```

#### オプション3. 各テストクラスのsetUp()や個別のテストメソッドの中で呼び出す

当然ながら、オプション1やオプション2のような全てのテストに適用される方法だけでなく、必要な時のみ明示的に呼ぶ方法もあります。

```php
use DG\BypassFinals;

class SlackTest extends TestCase
{
    public function testCanSendSlackMessage(): void
    {
        // finalクラスのバイパスを有効化する
        BypassFinals::enable();

        // finalなクラスをモックする
        $mock = Mockery::mock(
            SlackClient::class,
            function (MockInterface $m) {
                // モックの振る舞いを定義する

                return $m;
            }
        );

        // モックを利用する
    }
}
```

### 動作確認

問題なくモックできるようになりました。

```
   PASS  Tests\Unit\BookingServiceTest
  ✓ can send slack message                                                                                                                                                                         0.14s  

  Tests:    1 passed (2 assertions)
  Duration: 0.20s
```

# readonlyクラスのモックもできるようになる！

このパッケージ、ver 1.5からreadonlyクラスのモックも可能にしてくれる機能が備わったようです。

#### 有効化しない場合

readonlyじゃないクラスはreadonlyクラスを継承できないよ、というエラーメッセージが出てしまいます。

```
PHP Fatal error:  Non-readonly class Mockery_0_App_Services_SlackService cannot extend readonly class App\Services\SlackService in /var/www/html/vendor/mockery/mockery/library/Mockery/Loader/EvalLoader.php(24) : eval()'d code on line 21
```

#### 有効化すると

readonlyなクラスやfinal readonlyなクラスをモックできるようになりました！

```php
final readonly class SlackService
{
```

```
   PASS  Tests\Unit\BookingServiceTest
  ✓ can send slack message                                                                                                                                                                         0.14s  

  Tests:    1 passed (2 assertions)
  Duration: 0.20s
```


# 参考文献
https://github.com/dg/bypass-finals

https://tomasvotruba.com/blog/2019/03/28/how-to-mock-final-classes-in-phpunit