Laravel でテストしていて A facade root has not been set.
というエラーメッセージに遭遇したことがありました。原因は単純でしたが、思いの外、ハマってしまったので、メモしておきます。
まとめ
事象
この記事では、以下のエラーメッセージが出た場合を扱います。
RuntimeException : A facade root has not been set.
対処法
-
use PHPUnit\Framework\TestCase
をuse Tests\TestCase
に変更する。 - 自前で
setUp()
を実装した場合は、parent::setUp()
を必ずコールする。
前提
- Laravel 6.18.10
1. useするクラスを変更する
PHPUnit\Framework\TestCase
は Laravel ではなく PHPUnit のクラスです。なので、これを use
しても、ファサードを動かすことができず、エラーとなります。
そのため、Laravel のクラスである Tests\TestCase
を use
する必要があります。
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()
をコールするのを忘れると、ファサードを動かすことができず、エラーとなります。
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\TestCase
を use PHPUnit\Framework\TestCase
へ変更することが可能です。
とはいえ、Laravel のコア開発者が、どういう意図で、テストコードのサンプルを用意しているかは知っておいて損はないと思います。3
余談2: setUp()は何をしているのか?
ファサードのメソッドをコールすると、 Illuminate\Support\Facades\Facade
の __callStatic()
がコールされます。 __callStatic()
は以下のようになっており、 $instance
が空の場合に A facade root has not been set.'
という例外を送出することになっていることが分かります。
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()
に辿り着きます。
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\TestCase
の setUp()
がコールされます。 setUp()
は、 refreshApplication()
をコールします。
protected function setUp(): void
{
if (! $this->app) {
$this->refreshApplication();
}
// ...
}
refreshApplication()
は、トレイト Tests\CreatesApplication
の createApplication()
をコールします。
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()
がコールされます。
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
// ...
}
bootstrappers()
で返される配列の中には、 Illuminate\Foundation\Bootstrap\RegisterFacades
が含まれています。
Illuminate\Foundation\Application
の bootstrapWith()
は、 bootstrappers()
が返した配列の各要素に対して、名前解決した上で、その要素のクラスの bootstrap()
を実行していきます。
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\RegisterFacades
の bootstrap()
は、以下のようになっています。
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\Facade
の setFacadeApplication()
をコールしている箇所です。ここで Facade
のプロパティ $app
がセットされているのです。
public static function setFacadeApplication($app)
{
static::$app = $app;
}
まとめると、テストクラスを実行したときにコールされる setUp()
では、(長い旅の末に) Facade
の setFacadeApplication()
をコールすることで、 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}()
-
CHANGELOG には "Use PHPUnit TestCase and in-memory DB (#5169)" と一応記載されていますが、ここまで見ている人は少ないと思います(笑) ↩
-
提案者は "Feature" と "Integration" を区別したかったようです。しかし、Taylor が「個人的には Integration テストは "Feature" ディレクトリに置く。そのディレクトリは元々そういう目的のものだから」と述べたことや、テストの分け方にはさまざまな考え方があることから、
use
するクラスの変更のみに留めたようです。 ↩ -
自分は、テストコードを書かない 前近代的な 環境しか経験していませんが、テストの考え方は本当に各案件ごとだなあと思います。そもそも、テスト工程の名称からして、同じだったことはありません。個人的には「単体テスト」と「結合テスト」という呼び方に親しみがあり、「単体テスト」は各機能をテストするもの(DB の更新等も含む)、「結合テスト」は複数の機能をつなげてテストするものというイメージがあります。ただ、それは「オブジェクト指向ではなく手続き型で開発し、テストも手作業で実施する」という案件のお話です。テストコードを書く場合は、また違うと思いますし、むしろ開発やテストの方法に一番合ったテストを行うべきだと思います。 ↩