Laravelのユニットテストでconfig()やasset()を使うとエラーが出る

2024.05.14 09:00
2024.05.10 13:41
Laravelのユニットテストでconfig()やasset()を使うとエラーが出る

LaravelのUnitテストでasset()のようなbladeで使うメソッドを使用したクラスをテストしようとしたら一筋縄ではいかなかったので、解決した方法をメモします。

まず今回のテスト対象のクラスはこんな感じ。

<?php declare(strict_types=1);

namespace App\Models;

class Hoge
{
    public static function hoge(bool $var): string
    {
        $url = asset('/');

        if($var) return $url . 'true';

        return $url . 'false';
    }
}

boolを代入して真偽をURLの後ろにくっつけるだけのクラスです。
URLはbladeでよく使う「asset」で取得しています。

そしてテストコードは普通こんな感じ。

(※privateやstaticをテストする場合はreflectionを使ってテストをしますprivateメソッドのテストについては以前の記事「PHPUnitで単体テストを書いてみる〜privateメソッド編」をご覧ください。)

<?php declare(strict_types=1);

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Models\TestClass;

class HogeTest extends TestCase
{
    private TestClass $testClass;
    private ReflectionClass $reflection;

    public function setUp(): void
    {
        // 必須
        parent::setUp();
        
        // テスト対象のクラスインスタンスを作成
        $this->testClass = new TestClass();

        // privateメソッドを読み込むためにReflectionクラスを使う
        $this->reflection = new ReflectionClass($this->testClass);
    }

    public function test_hoge(): void
    {
        // Reflectionクラスを使ってprivateメソッドを読み込む
        $method = $this->reflection->getMethod('hoge');

        // invokeメソッドを使って、privateメソッドを実行する
        $result = $method->invoke($this->testClass, true);

        // 評価
        $this->assertSame('http://localhost:8000/true', $result);
    }

trueを代入したときのテストを書きました。
しかし、こんなエラーがでてうまく行きませんでした。

Illuminate\Contracts\Container\BindingResolutionException: Target class [url] does not exist.

urlが無いって言われています。なるほど、assetがテストではうまく動いていないようですね。
調べてみると、テストの方で「createApplication()」を読み込めばいいようです。
ということで、「tests/CreatesApplication.php」を作ります。

<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        $app = require __DIR__.'/../bootstrap/app.php';

        $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
        
        return $app;
    }
}

そしてテストでこれを読み込みます。

<?php declare(strict_types=1);

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Models\TestClass;
use Tests\CreatesApplication; // [+]追加

class HogeTest extends TestCase
{
    // [+]追加
    use CreatesApplication;

    private TestClass $testClass;
    private ReflectionClass $reflection;

    public function setUp(): void
    {
        // 必須
        parent::setUp();

        // [+]追加
        $this->createApplication();

        // テスト対象のクラスインスタンスを作成
        $this->testClass = new TestClass();

        // privateメソッドを読み込むためにReflectionクラスを使う
        $this->reflection = new ReflectionClass($this->testClass);
    }

    public function test_hoge(): void
    {
        // Reflectionクラスを使ってprivateメソッドを読み込む
        $method = $this->reflection->getMethod('hoge');

        // invokeメソッドを使って、privateメソッドを実行する
        $result = $method->invoke($this->testClass, true);

        // 評価
        $this->assertSame('http://localhost:8000/true', $result);
    }

これでテストを通してみましょう。
また違うエラーが出てきました。

Test code or tested code did not remove its own error handlers

Test code or tested code did not remove its own exception handlers
Time: 00:00.315, Memory: 26.00 MB

There was 1 risky test:

1) Tests\HogeTest::test_hoge
* Test code or tested code did not remove its own error handlers

* Test code or tested code did not remove its own exception handlers

エラーとexceptionは消せないよ?みたいなことが言われていますね。
消してはいない、、じゃあ足りないんですね。
ということでCreateApplication.phpに以下を追加してみましょう。

    protected function restoreExceptionHandler(): void
    {
        while (true) {
            $previousHandler = set_exception_handler(static fn() => null);
            restore_exception_handler();
            if ($previousHandler === null) {
                break;
            }
            restore_exception_handler();
        }
    }

    protected function restoreErrorHandler(): void
    {
        while (true) {
            $previousHandler = set_error_handler(static fn() => null);
            restore_error_handler();
            $isPhpUnitErrorHandler = ($previousHandler instanceof \PHPUnit\Runner\ErrorHandler);
            if ($previousHandler === null || $isPhpUnitErrorHandler) {
                break;
            }
            restore_error_handler();
        }
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        $this->restoreErrorHandler();
        $this->restoreExceptionHandler();
    }

createApplication.phpのコード全体はこんな感じ。

<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        $app = require __DIR__.'/../bootstrap/app.php';

        $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
        
        return $app;
    }

    protected function restoreExceptionHandler(): void
    {
        while (true) {
            $previousHandler = set_exception_handler(static fn() => null);
            restore_exception_handler();
            if ($previousHandler === null) {
                break;
            }
            restore_exception_handler();
        }
    }

    protected function restoreErrorHandler(): void
    {
        while (true) {
            $previousHandler = set_error_handler(static fn() => null);
            restore_error_handler();
            $isPhpUnitErrorHandler = ($previousHandler instanceof \PHPUnit\Runner\ErrorHandler);
            if ($previousHandler === null || $isPhpUnitErrorHandler) {
                break;
            }
            restore_error_handler();
        }
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        $this->restoreErrorHandler();
        $this->restoreExceptionHandler();
    }
}

これでもう一度テスト実行!

無事うまく行きました!
なかなかしんどかったですね。

今回は以上です!