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
というパッケージです。
使い方は以下の通りです。
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なクラスをモックすることができます。
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
require_once __DIR__ . '/../vendor/autoload.php';
// final / readonlyのバイパス有効化
DG\BypassFinals::enable();
オプション3. 各テストクラスのsetUp()や個別のテストメソッドの中で呼び出す
当然ながら、オプション1やオプション2のような全てのテストに適用される方法だけでなく、必要な時のみ明示的に呼ぶ方法もあります。
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なクラスをモックできるようになりました!
final readonly class SlackService
{
PASS Tests\Unit\BookingServiceTest
✓ can send slack message 0.14s
Tests: 1 passed (2 assertions)
Duration: 0.20s
参考文献