3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

よりそうAdvent Calendar 2023

Day 23

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

Posted at

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

参考文献

3
2
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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?