25
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Laravelで "A facade root has not been set." というエラーが出た場合の対処法

Last updated at Posted at 2020-05-04

Laravel でテストしていて A facade root has not been set. というエラーメッセージに遭遇したことがありました。原因は単純でしたが、思いの外、ハマってしまったので、メモしておきます。

まとめ

事象

この記事では、以下のエラーメッセージが出た場合を扱います。

RuntimeException : A facade root has not been set.

対処法

  1. use PHPUnit\Framework\TestCaseuse Tests\TestCase に変更する。
  2. 自前で setUp() を実装した場合は、 parent::setUp() を必ずコールする。

前提

  • Laravel 6.18.10

1. useするクラスを変更する

PHPUnit\Framework\TestCase は Laravel ではなく PHPUnit のクラスです。なので、これを use しても、ファサードを動かすことができず、エラーとなります。

そのため、Laravel のクラスである Tests\TestCaseuse する必要があります。

Tests\Unit\SampleServiceTest
namespace Tests\Unit;

// PHPUnit ではなく Laravelの TestCase を使う。
// use PHPUnit\Framework\TestCase;
use Tests\TestCase;
use App\Http\Services\SampleService;

class SampleServiceTest extends TestCase
{
    /**
     * @test
     */
    public function 製品一覧を取得できること()
    {
        $service = new SampleService();

        // DB ファサードを使うメソッド
        $result = $service->fetchProducts();

        $expected = [
            (object)['name' => 'りんご'],
            (object)['name' => 'オレンジ'],
        ];

        $this->assertEquals($expected, $result);
    }
}

2. 祖先クラスのsetUp()をコールする

Laravel が提供するクラス Tests\TestCase は、 Illuminate\Foundation\Testing\TestCase (以下、 祖先クラス と呼びます)を継承しています。そこで定義されている setUp() では、Laravel アプリケーションを起動するためのさまざまな事前準備を行っており、その中にファサードの登録も含まれています。

テストクラスを実行すると、PHPUnit は、各テストメソッドの前に setUp() をコールします。自前で setUp() を実装しなかった場合は、祖先クラスの setUp() が自動的にコールされ、ファサードが正常に動作します。

しかし、自前で setUp() を実装した場合は、祖先クラスの setUp() よりもテストクラスの setUp() が優先されます。

そのため、自前で実装した setUp() で祖先クラスの setUp() をコールするのを忘れると、ファサードを動かすことができず、エラーとなります。

Tests\Unit\SampleServiceTest
namespace Tests\Unit;

use Tests\TestCase;
use App\Http\Services\SampleService;
use Illuminate\Support\Facades\DB;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class SampleServiceTest extends TestCase
{
    public function setUp(): void
    {
        // 祖先クラスの setUp() を忘れずにコールする。
        parent::setUp();
    }

    /**
     * @test
     */
    public function 製品一覧を取得できること()
    {
        $service = new SampleService();
        $result = $service->fetchProducts();

        $expected = [
            (object)['name' => 'りんご'],
            (object)['name' => 'オレンジ'],
        ];

        $this->assertEquals($expected, $result);
    }
}

余談

余談1: Laravel 6以降の変更

これまで php artisan make:test --unit を実行すると、use Tests\TestCase となっているテストコードが生成されていました。

しかし、Laravel 6 の途中から1 use PHPUnit\Framework\TestCase へと変更されました( コミット )。

比較的最近の記事でも、 Unit ディレクトリで DB に接続するテストコードを紹介しているものがありますが、上記の変更を意識せずに php artisan make:test --unit でテストコードを生成して実装すると、 A facade root has not been set. というエラーが表示されます。

プルリクエスト や、作者 Taylor Otwell の コメント によると、 tests/Unit ディレクトリと tests/Feature ディレクトリは以下のように使い分けるのが推奨されているようです。2

Unit

単一のオブジェクトや関数をテストする。外部に依存しない。依存するものは一部をモック化する。

Feature

異なるコンポーネントをつなげて振る舞いをテストする。データベースのような追加のセットアップが必要になる。

ただし、別のプルリクエストで コメント されているように、必ずしも、この考え方に縛られる必要はありません。個々のプロジェクトで、必要に応じて、自由に use Tests\TestCaseuse PHPUnit\Framework\TestCase へ変更することが可能です。

とはいえ、Laravel のコア開発者が、どういう意図で、テストコードのサンプルを用意しているかは知っておいて損はないと思います。3

余談2: setUp()は何をしているのか?

ファサードのメソッドをコールすると、 Illuminate\Support\Facades\Facade__callStatic() がコールされます。 __callStatic() は以下のようになっており、 $instance が空の場合に A facade root has not been set.' という例外を送出することになっていることが分かります。

Illuminate\Support\Facades\Facade
public static function __callStatic($method, $args)
{
    $instance = static::getFacadeRoot();

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }

    return $instance->$method(...$args);
}

$instance の作られ方を辿っていくと、 resolveFacadeInstance() に辿り着きます。

Illuminate\Support\Facades\Facade
protected static function resolveFacadeInstance($name)
{
    if (is_object($name)) {
        return $name;
    }

    if (isset(static::$resolvedInstance[$name])) {
        return static::$resolvedInstance[$name];
    }

    if (static::$app) {
        return static::$resolvedInstance[$name] = static::$app[$name];
    }
}

最後の if (static::$app)true ならば、Illuminate\Foundation\Application インスタンス経由で、ファサードのインスタンスを取得しています。 これはどのように作られているのでしょうか?

ここからは、テストクラスが実行される順番で見ていきましょう。

テストクラスを実行すると、祖先クラス Illuminate\Foundation\Testing\TestCasesetUp() がコールされます。 setUp() は、 refreshApplication() をコールします。

Tests\TestCase
protected function setUp(): void
{
    if (! $this->app) {
        $this->refreshApplication();
    }

    // ...
}

refreshApplication() は、トレイト Tests\CreatesApplicationcreateApplication() をコールします。

Tests\CreatesApplication
public function createApplication()
{
    $app = require __DIR__.'/../bootstrap/app.php';

    $app->make(Kernel::class)->bootstrap();

    return $app;
}

$app->make(Kernel::class) の名前解決の結果、 Illuminate\Foundation\Console\Kernel が解決されます。そして、その bootstrap() がコールされます。

Illuminate\Foundation\Console\Kernel
public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
    }

    // ...
}

bootstrappers() で返される配列の中には、 Illuminate\Foundation\Bootstrap\RegisterFacades が含まれています。

Illuminate\Foundation\ApplicationbootstrapWith() は、 bootstrappers()
が返した配列の各要素に対して、名前解決した上で、その要素のクラスの bootstrap() を実行していきます。

Illuminate\Foundation\Application
public function bootstrapWith(array $bootstrappers)
{
    $this->hasBeenBootstrapped = true;

    foreach ($bootstrappers as $bootstrapper) {
        $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);

        $this->make($bootstrapper)->bootstrap($this);

        $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
    }
}

Illuminate\Foundation\Bootstrap\RegisterFacadesbootstrap() は、以下のようになっています。

Illuminate\Foundation\Bootstrap\RegisterFacades
public function bootstrap(Application $app)
{
    Facade::clearResolvedInstances();

    Facade::setFacadeApplication($app);

    AliasLoader::getInstance(array_merge(
        $app->make('config')->get('app.aliases', []),
        $app->make(PackageManifest::class)->aliases()
    ))->register();
}

注目すべきは、 Illuminate\Support\Facades\FacadesetFacadeApplication() をコールしている箇所です。ここで Facade のプロパティ $app がセットされているのです。

Illuminate\Support\Facades\Facade
public static function setFacadeApplication($app)
{
    static::$app = $app;
}

まとめると、テストクラスを実行したときにコールされる setUp() では、(長い旅の末に) FacadesetFacadeApplication() をコールすることで、 Facade$app をセットしています。そのため、 __callStatic$instance を取得することができるのです。

最後に、 setFacadeApplication() がコールされるまでのスタックトレースを添付しておきます。

Facade.php:242, Illuminate\Support\Facades\Facade::setFacadeApplication()
RegisterFacades.php:22, Illuminate\Foundation\Bootstrap\RegisterFacades->bootstrap()
Application.php:219, Illuminate\Foundation\Application->bootstrapWith()
Kernel.php:320, App\Console\Kernel->bootstrap()
CreatesApplication.php:18, Tests\Unit\SampleServiceTest->createApplication()
TestCase.php:102, Tests\Unit\SampleServiceTest->refreshApplication()
TestCase.php:79, Tests\Unit\SampleServiceTest->setUp()
TestCase.php:1031, Tests\Unit\SampleServiceTest->runBare()
TestResult.php:691, PHPUnit\Framework\TestResult->run()
TestCase.php:763, Tests\Unit\SampleServiceTest->run()
TestSuite.php:597, PHPUnit\Framework\TestSuite->run()
TestSuite.php:597, PHPUnit\Framework\TestSuite->run()
TestRunner.php:621, PHPUnit\TextUI\TestRunner->doRun()
Command.php:204, PHPUnit\TextUI\Command->run()
Command.php:163, PHPUnit\TextUI\Command::main()
phpunit:61, {main}()
  1. CHANGELOG には "Use PHPUnit TestCase and in-memory DB (#5169)" と一応記載されていますが、ここまで見ている人は少ないと思います(笑)

  2. 提案者は "Feature" と "Integration" を区別したかったようです。しかし、Taylor が「個人的には Integration テストは "Feature" ディレクトリに置く。そのディレクトリは元々そういう目的のものだから」と述べたことや、テストの分け方にはさまざまな考え方があることから、 use するクラスの変更のみに留めたようです。

  3. 自分は、テストコードを書かない 前近代的な 環境しか経験していませんが、テストの考え方は本当に各案件ごとだなあと思います。そもそも、テスト工程の名称からして、同じだったことはありません。個人的には「単体テスト」と「結合テスト」という呼び方に親しみがあり、「単体テスト」は各機能をテストするもの(DB の更新等も含む)、「結合テスト」は複数の機能をつなげてテストするものというイメージがあります。ただ、それは「オブジェクト指向ではなく手続き型で開発し、テストも手作業で実施する」という案件のお話です。テストコードを書く場合は、また違うと思いますし、むしろ開発やテストの方法に一番合ったテストを行うべきだと思います。

25
10
1

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
25
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?