Symfony Advent Calendar 2021 day 20の記事です!
昨日は @polidogさんのSymfony PassportでFirebase Authentication認証を使う でした。
SymfonyWorldにて、symfonycastsで有名なweaverryanさんのテストのセッションがあり、 zenstruck/foundry
というテストフィクスチャ登録用のライブラリが紹介されていました。Laravelのfactoryを彷彿とさせる使用感ですが、個人的に長年愛用してきた liip/test-fixtures-bundle
の使い方が最近変わって、微妙に使いにくくなったのが気になっていたので、新しいfoundryの使い方を調べてみました。
※ 実際のソースコードは https://github.com/77web/symfony-foundry-playground にて公開しています。
zenstruck/foundryを追加
$ composer req --dev zenstruck/foudndry
bundleっぽくない名前ですが、flexによって config/bundles.php
にバンドルとしても追加されていました。
+ Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true],
ファクトリを作る
ファクトリクラスは --test
をつけると tests/
の下に、 --test
なしだと src/
の下に作られます。今回はテスト用のfixtureのためのファクトリを作りたいので --test
つきで実行しました。
$ bin/console make:factory --test
Entity class to create a factory for:
[0] App\Entity\Task
> 0
created: tests/Factory/TaskFactory.php
Success!
Next: Open your new factory and set default values/states.
Find the documentation at https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
作られたTaskFactoryクラスはこんな感じです。
<?php
namespace App\Tests\Factory;
// ...
final class TaskFactory extends ModelFactory
{
public function __construct()
{
parent::__construct();
// TODO inject services if required (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services)
}
protected function getDefaults(): array
{
return [
// TODO add your default values here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories)
'title' => self::faker()->text(),
];
}
protected function initialize(): self
{
// see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
return $this
// ->afterInstantiate(function(Task $task): void {})
;
}
protected static function getClass(): string
{
return Task::class;
}
}
機能テストでファクトリを使ってみる
機能テストでファクトリクラスを使うためには、まずテストクラスに Zenstruck\Foundry\Test\Factories
をuseさせる必要があります。
ファクトリの使い方は2種類あります。
TaskFactory::createOne()
…1件のTaskを作るTaskFactory::createMany()
…指定件数のTaskを作る
また、ファクトリにフィクスチャを作らせる前に static::createClient()
を実行しておきましょう。
<?php
namespace App\Tests\Controller\TaskController;
+ use Zenstruck\Foundry\Test\Factories;
class CompletedTest extends WebTestCase
{
use Factories;
public function test()
{
$client = static::createClient();
+ // 1件未完了タスクを作る
+ TaskFactory::createOne(['completedAt' => null]);
+ // 2件完了済タスクを作る
+ TaskFactory::createMany(2, ['completedAt' => new \DateTimeImmutable('yesterday')]);
$crawler = $client->request('GET', '/task/completed');
$this->assertResponseIsSuccessful();
$this->assertTrue($crawler->filter('title')->text() === '完了したタスク');
+ $this->assertEquals(2, $crawler->filter('li')->count(),'完了済の2件のみ');
}
}
繰り返しテストを実行するために(テスト用DBのリセット)
テストが通ったと喜んで、再度実行したら落ちました 😅
There were 2 failures:
1) App\Tests\Controller\TaskController\CompletedTest::test
完了済の2件のみ
Failed asserting that 4 matches expected 2.
/Users/hiromi/projects/tryout-foundry/tests/Controller/TaskController/CompletedTest.php:27
2) App\Tests\Controller\TaskController\IndexTest::test
未完了の1件のみ
Failed asserting that 3 matches expected 1.
/Users/hiromi/projects/tryout-foundry/tests/Controller/TaskController/IndexTest.php:27
FAILURES!
Tests: 2, Assertions: 6, Failures: 2.
どうやら2回目に実行すると前回実行したデータが残っているようです。(しかもテストが2つあるので、それぞれ2回ずつtaskが作られたデータが残っています)
テストごとにデータベースをリフレッシュするには Zenstruck\Foundry\Test\ResetDatabase
traitをuseする必要があります。
なお、公式ドキュメントでは「全部のテストケースに2つのtraitをuseするのは大変なので、自分用のWebTestCaseを作ると良い」と書かれていました。
<?php
namespace App\Tests\Controller\TaskController;
use Zenstruck\Foundry\Test\Factories;
+ use Zenstruck\Foundry\Test\ResetDatabase;
class CompletedTest extends WebTestCase
{
use Factories;
+ use Factories, RefreshDatabases;
public function test()
{
DAMADoctrineTestBundle https://github.com/dmaicher/doctrine-test-bundle を使うと、テストケース全体をtransactionで囲って高速化もできるようです(試してません)
IDの強制セットができる
以前 Doctrineのエンティティを対象にしたテスト方法あれこれ で検討した、エンティティにidのsetterメソッドを作らなかったときにIDをセットする方法が、foundryではそのまま用意されていました。 forceSet()
メソッドを使うことで、setterのないプロパティにも値をセットすることができます。
<?php
// これはエラー
TaskFactory::createOne(['id' => 999]);
// これはOK
$taskProxy = TaskFactory::createOne();
$taskProxy->forceSet('id', 999);
ユニットテストで使う
このファクトリですが、なんとユニットテスト(SymfonyのKernelTestCaseでもWebTestCaseでもない、PHPUnitの普通のTestCaseクラス)でも使えます。 ユニットテストでfoundryのファクトリを利用して作られたエンティティは、DBに保存されず(ここがLaravelのfactoryとは違うところですね 😁)、DB接続のない環境でもテストが通ります。
ただし、 XXXFactory::createOne()
の返り値はエンティティそのものでなくfoundryの Proxy
のインスタンスになっているため、そのままサービスクラスに渡すことはできないので注意が必要です。
XXXFactory::createOne()->object()
でエンティティ自体のインスタンスを取得できます。
<?php
// ...
use Factories;
public function test()
{
$task = TaskFactory::createOne()->object();
$this->emP->persist(Argument::that(function (Task $task) {
return $task->getCompletedAt() !== null;
}))->shouldBeCalled();
$this->emP->flush()->shouldBeCalled();
$this->getSUT()->markComplete($task);
}
今までの生エンティティを直接使う書き方(下記)より可読性が良い気はします。
<?php
$task = (new Task())
->setTitle('foo')
->setDescription('bar')
;
参考リンク
- zenstruck/foundryのレポジトリ https://github.com/zenstruck/foundry
- zenstruck/foundryの公式ドキュメント https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html