Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CakePHP Fixture Factories の登場によって変化する、PHPプロジェクトにおけるテストフィクスチャ管理の選択肢 / Test Fixture Management in PHP Project

CakePHP Fixture Factories の登場によって変化する、PHPプロジェクトにおけるテストフィクスチャ管理の選択肢 / Test Fixture Management in PHP Project

Takuya Obara (barbara)

March 31, 2022
Tweet

More Decks by Takuya Obara (barbara)

Other Decks in Programming

Transcript

  1. 誰? takoba / ばーばら 🌹 🏢 コネヒト株式会社 󰳕 Webアプリケーション /

    PHP / ちょっとReact / たまにUXデザイン 🏓 インターネット / 音楽 / カレーライス / 銭湯 / Jリーグ / ポッドキャスト @takoba @takoba_
  2. • cf. xUnit - Wikipedia ◦ > コンピュータプログラムの単体テスト(ユニットテスト)を行うためのテ スティングフレームワークの総称 ◦

    この中の xUnitの設計 項の中に"テストスイート"、"テストの実行"、"ア サーション(表明、検証)"と並んで"テストフィクスチャ"がある • 各種言語にxUnitに準拠したテスティングフレームワークが存在する ◦ JUnit(Java向けテスティングフレームワーク) ◦ test-unit / Test::Unit(Ruby向けテスティングフレームワーク) ◦ MochiKit(JavaScript向けテスティングフレームワーク) ◦ PHPUnit(PHP向けテスティングフレームワーク) (おさらい)xUnitとは?
  3. • cf. xUnit - Wikipedia ◦ > テストを実行、成功させるために必要な状態や前提条件の集合を、フィク スチャ(英語版)と呼ぶ。これらはテストコンテキストとも呼ばれる。開 発者はテストの実行前にテストに適した状態を整え、テスト実行後に元の

    状態を復元することが望ましい。 • テストフィクスチャは前提条件となるもの全てを指してる ◦ データベースにレコードを登録するアプリだったら、データベース(およ び事前に必要になるレコード)がテストフィクスチャ ◦ 文字をトークナイズするライブラリだったら、自然言語のテキストたちが テストフィクスチャ ▪ 小さなライブラリなら単なる文字列とか配列とかもそう テストフィクスチャとは
  4. • cf. xUnit - Wikipedia ◦ > テストを実行、成功させるために必要な状態や前提条件の集合を、フィク スチャ(英語版)と呼ぶ。これらはテストコンテキストとも呼ばれる。開 発者はテストの実行前にテストに適した状態を整え、テスト実行後に元の

    状態を復元することが望ましい。 • テストフィクスチャは前提条件となるもの全てを指してる ◦ データベースにレコードを登録するアプリだったら、データベース(およ び事前に必要になるレコード)がテストフィクスチャ ◦ 文字をトークナイズするライブラリだったら、自然言語のテキストたちが テストフィクスチャ ▪ 小さなライブラリなら単なる文字列とか配列とかもそう テストフィクスチャとは xUnitに準拠したユニットテストでは、 テストフィクスチャはなくてはならないもの(=前提)
  5. • 「どんくらい昔からやってるのそれ?」 ◦ とりあえず Rails 2.x の段階ではすでにテストフィクスチャについて RailsGuides に言及されていた ので、まあフルスタックフレームワークが登

    場した初期の頃(2008年くらい)から存在してそう ◦ そして rails/rails v1.0.0 の activerecord にも普通にfixtures.rb があった ので、rails正式リリース当初(2005年くらい)から存在してそう ▪ @t_wada「エリック・ガンマとケント・ベックがチューリッヒからア トランタまでのフライトの中で"SUnitをJUnitに移植する"という伝説の ペアプロを行った、ってのがあって、それが今年(2022年)で25年」 • cf. 9. The 20th Anniversary of TDD by texta.fm Webアプリケーションのテストフィクスチャ
  6. • 「うん...そうなんだ...」 ◦ @t_wada「テスト駆動開発(TDD; Test-Driven Development)が生まれた のも2002年」とも言ってるので、割とフルスタックフレームワークでテス トが整備されたのってRuby on Railsは割と早かったんだと思う

    ◦ @t_wada「TDD原著が出たのも言葉が生まれた直後。2002年だった、と」 ◦ もしかしたら初期のStrutsとかで用意されてる可能性はあるけど調べきれ ず Webアプリケーションのテストフィクスチャ
  7. • Laravelの場合はどういう仕組みになってる? ◦ おっと?? Laravel 9 ではModel Factories という概念を使ってるね?? ◦

    cf. Defining Model Factories | Database Testing - Laravel - The PHP Framework For Web Artisans ▪ Laravel 5.3くらいからあるっぽい?誰か補足して... • cf. Writing Factories | Database Testing - Laravel - The PHP Framework For Web Artisans Laravelのテストフィクスチャ
  8. • セットアップについては Test fixture - Wikipedia に載っていた • > Test

    fixtures can be set up three different ways: in-line, delegate, and implicit. ◦ テストフィクスチャは”インライン”、”委譲”、”暗黙的”といった3通りの方 法でセットアップできるんやで 3つのセットアップ戦略
  9. • こんなかんじの構成を用いて説明するよ ◦ App ▪ class UsersClient ▪ class User

    ◦ TestSuite ▪ class TestCase (class PHPUnit\Framework\TestCase のイメージ) ◦ Test ▪ class UsersClientTest extends TestCase ※このあとお見せするソースコードの前提
  10. <?php declare(strict_types=1); namespace TestSuite; class TestCase { /** * @throws

    ExpectationFailedException */ public function assertSame($expected, $actual): void { if ($expected !== $actual) { throw new ExpectationFailedException('failure asserting that two variables'); } echo sprintf( 'success asserting that two variables %s and %s', var_export($expected, true) ?? 'null', var_export($actual, true) ?? 'null', ) . "\n"; } } class ExpectationFailedException extends \Exception {}
  11. <?php declare(strict_types=1); namespace App; class UsersClient { private $data =

    []; public function getUserById(string $id): ?User { if (!array_key_exists($id, $this->data)) { return null; } return $this->data[$id]; } public function setData(array $data): self { $isVector = array_values($data) === $data; $this->data = $isVector ? array_reduce(function ($carry, User $user) { $carry[$user->id] = $user; return $carry; }, $data) : $data; return $this; } } <?php declare(strict_types=1); namespace App; class User { private array $fields = []; public function __construct(array $fields) { $this->fields = $fields; } public function &__get(string $field) { return $this->fields[$field]; } public function __set(string $field, $value) { $this->fields[$field] = $value; } }
  12. • 日本語で扱うなら「インライン・セットアップ」でよいかな • > In-line setup creates the test fixture

    in the same method as the rest of the test. While in-line setup is the simplest test fixture to create, it leads to duplication when multiple tests require the same initial data. ◦ 要は、テストケース内で定義される形式 In-line setup
  13. • cf. Inline Setup at XUnitPatterns.com • テストメソッドから直接テストフィクスチャ にアクセスする、ってのが図示されてる •

    > Setting up the test fixture inline in each test is the most obvious way to build it. FYI: XUnitPatterns.comにはさっきのsetupも解説あり〼
  14. <?php declare(strict_types=1); namespace Test; use App\User; use App\UsersClient; use TestSuite\TestCase;

    class UsersClientTest extends TestCase { public function test_getUserById(): void { // given $fixture = [ new User(['id' => 1, 'name' => 'John']), new User(['id' => 2, 'name' => 'Paul']), ]; $client = (new UsersClient())->setData($fixture); // when $actual = $client->getUserById('1'); // then $expected = new User(['id' => 1, 'name' => 'John']); $this->assertEqual($expected, $actual); $this->assertEqual($expected->id, $actual->id); $this->assertEqual($expected->name, $actual->name); } } (new UsersClientTest())->test_getUserById(); https://3v4l.org/FhFpp テストケースの内部で定義してるから In-line setup
  15. • 日本語で扱うなら「委譲セットアップ」でよいかな • > Delegate setup places the test fixture

    in a separate standalone helper method that is accessed by multiple test methods. ◦ 複数のテストメソッドから呼ばれるstandaloneなヘルパーメソッドを経由 して呼び出される形式 Delegate setup
  16. • cf. Delegated Setup at XUnitPatterns.com • テストメソッドからは”Utility Method”を解 してテストフィクスチャにアクセスする、っ

    てのが図示されてる • > We are using a Fresh Fixture approach to build a Minimal Fixture for the use of this one test and we'd like to avoid Test Code Duplication. FYI: XUnitPatterns.comにはさっきのsetupも解説あり〼
  17. <?php declare(strict_types=1); namespace Test; use App\User; use App\UsersClient; use TestSuite\TestCase;

    class UsersClientTest extends TestCase { public function test_getUserById(): void { // given $fixture = $this->validUsersDataProvider(); $client = (new UsersClient())->setData($fixture); // when $actual = $client->getUserById('1'); // then $expected = new User(['id' => 1, 'name' => 'John']); $this->assertEqual($expected, $actual); $this->assertEqual($expected->id, $actual->id); $this->assertEqual($expected->name, $actual->name); } public function validUsersDataProvider(): array { return [ new User(['id' => 1, 'name' => 'John']), new User(['id' => 2, 'name' => 'Paul']), ]; } } (new UsersClientTest())->test_getUserById(); https://3v4l.org/bdABQ テストデータの生成を別のメソッドに委譲してるので Delegate setup
  18. • 日本語で扱うなら「暗黙的セットアップ」でよいかな • > Implicit setup places the test fixture

    in a setup method which is used to set up multiple test methods. This differs from delegate setup in that the overall setup of multiple tests is in a single setup method where the test fixture gets created rather than each test method having its own setup procedures and linking to an external test fixture. ◦ 単一のセットアップ処理で生成されて、それが複数のテストメソッドから 呼び出される ◦ 多くのアプリケーションフレームワークで標準装備されてるのは、この形 式のテストフィクスチャ管理になるはず Implicit setup
  19. • cf. Implicit Setup at XUnitPatterns.com • テストメソッドからではなく setUp() がテス

    トフィクスチャにアクセスする、ってのが図 示されてる • > SImplicit Setup is a way to reuse the fixture setup code for all the Test Methods in a Testcase Class. FYI: XUnitPatterns.comにはさっきのsetupも解説あり〼
  20. <?php declare(strict_types=1); namespace Test; use App\User; use App\UsersClient; use TestSuite\TestCase;

    class UsersClientTest extends TestCase { private $fixture; private $subject; public function setUp(): void { $this->fixture = [ new User(['id' => 1, 'name' => 'John']), new User(['id' => 2, 'name' => 'Paul']), ]; $this->subject = (new UsersClient())->setData($this->fixture); } public function test_getUserById(): void { // when $actual = $this->subject->getUserById('1'); // then $expected = current($this->fixture); $this->assertEqual($expected, $actual); } } $subject = new UsersClientTest(); $subject->setUp(); $subject->test_getUserById(); https://3v4l.org/OHsR1 テストケースの最初に暗黙的に定義しておくので Implicit setup
  21. <?php declare(strict_types=1); namespace TestSuite; class TestCase { protected function setUp():

    void { // do something... } /** * @throws ExpectationFailedException */ public function assertEqual($expected, $actual): void { if ($expected != $actual) { throw new ExpectationFailedException('failure asserting that two variables'); } echo sprintf( 'success asserting that two variables %s and %s', var_export($expected, true) ?? 'null', var_export($actual, true) ?? 'null', ) . "\n"; } } https://3v4l.org/OHsR1 こんなかんじでテストケースのベースになる classで setUp() をcallするように用意しておく
  22. • cf. Fresh Fixture at XUnitPatterns.com • > We design

    and build the test fixture such that only a single running of a single test will use it. We construct the fixture as part of running the test and tear down the fixture when the test has finished. ◦ テストケース実行するたびにビルドして、テストケース終わったら破棄 ◦ 基本的なフルスタックフレームワークはこれが前提だと思うで • > When each test creates a Fresh Fixture, it prevents Erratic Tests and is more likely to result in Tests as Documentation. ◦ Fresh Fixtureを採用すると、不規則なテストがなくなるし”Tests as Documentation(ドキュメントとしてのテスト)”となってよいわよ ◦ あれ?最強そうじゃん?? これらのsetupは”Fresh Fixture”という戦略の上にある
  23. • cf. abseil / Software Engineering at Google • Part.11

    Testing Overview の序文 ◦ > If you have a robust testing practice, you needn’t fear change—you can embrace it as an essential quality of developing software. The more and faster you want to change your systems, the more you need a fast way to test them. ▪ システムをスピード感持って変更するためには、より高速なテスト実 行環境が必要だよ • Fresh Fixtureは都度読み込むから実行コスト/速度がかかりそうね...?? そもそも、テストスイートに重要な観点って何?
  24. • cf. Software Design 2022年3月号|技術評論社 ◦ @t_wadaさんの「自動テストとテスト駆動開発、その全体像」の序盤 ◦ > 自動テストが必ず満たすべき性質が2つあります。自己検証可能

    (Self-Validating)であることと、繰り返し可能(Repeatable)であるこ と ▪ 繰り返し可能であるためにFresh Fixtureは必須になりそう ◦ > 自動テストに強く推奨される性質が2つあります。独立している (Independent / Isolated)ことと高速である(Fast)こと ▪ 独立していることを担保するためにテストフィクスチャも分離したい ▪ 高速であるためには、Fresh Fixtureだとオーバーヘッドが大きいかも そもそも、テストスイートに重要な観点って何?
  25. • cf. 現在時刻が関わるユニットテストから、テスト容易性設計を学ぶ - t-wadaの ブログ ◦ > Repeatable であるとは、テストを実行するだけで、いつでも、何回でも

    同じように動くということです。環境や時間によってテスト結果が変化し てしまう場合、そのテストは Repeatable ではありません。 テストコードが 変わっていないのに、状況によって通ったり通らなかったりするテスト は、書籍『xUnit Test Patterns』ではErratic Test (不安定なテスト)と 表現されています。 • 何度も繰り返し実行するので速度も大事だし、安定して動くことも大事 FYI: 「よいユニットテストとはRepeatable」
  26. • cf. Shared Fixture at XUnitPatterns.com ◦ > To execute

    an automated test, we require a text fixture that is well understood and completely deterministic. Setting up a Fresh Fixture can be time consuming, especially when dealing with complex system state stored in a test database. ▪ “Fresh Fixture”だと都度テストフィクスチャをsetupするから時間かか るのよね ▪ あっ基本的にデータ投入して用意しとくから使ってや!なあ兄ちゃん • これってテストケースの実行サイクルとは別に、”すでに用意され ているテストフィクスチャ”として扱う必要があるのよね 高速化のためのテストフィクスチャ戦略
  27. • Shared Fixtureを実現するには、”完全に決定論的であり、よく理解されたテス トフィクスチャ"が必要 ◦ cf. 決定論 - Wikipedia ▪

    > 決定論(英: determinism)とは、あらゆる出来事は、その出来事に 先行する出来事のみによって決定している、とする哲学的な立場。 ◦ 要は、状態とかが諸々固定されたテストフィクスチャが必要になる ▪ 「あれ?じゃあ同じテストケースでも、前提条件が異なるテストフィ クスチャが必要な時はどうするの...?」「...君のような勘のいいガキは 嫌いだよ」 決定論的なテストフィクスチャが必要なのだけど...
  28. • cf. Standard Fixture at XUnitPatterns.com ◦ > A Standard

    Fixture is a way to reuse the same fixture design in several tests without necessarily sharing the same fixture instance. ▪ 使い回せるような、Standardなテストフィクスチャを設計しような! • cf. Minimal Fixture at XUnitPatterns.com ◦ > A key part of understanding a test is understanding the test fixture and how it influences the expected outcome of the test. Tests are much easier to understand if the fixture is small and simple. ▪ 余計な要素を削ることでテストフィクスチャの内容を把握するぞ! • これらの戦略も決定論的にテストフィクスチャを整理するための戦略 高速化のためのテストフィクスチャ戦略
  29. • ここまでを整理すると ◦ 実行速度を求めるならば事前にセットアップしておけるShared Fixtureが 効果的なのはわかる ▪ でも決定論的ではなく多様な前提条件を表現する必要がある ▪ テストフィクスチャならばFresh

    Fixtureで用意するしかないのでは? • 要はトレードオフなのだと思う ◦ 決定論的なテストフィクスチャを”表現”するのそんなむずかしくない ▪ 内容を調整するのは大変だが、フィクスチャが洗練され高速化も期待 ◦ 多様なパターンがあるテストフィクスチャ群を表現するのが難しいので、 それが柔軟に表現できる"表現力があると便利!ってことじゃない?? なんかたくさんの用語が出てきたし、整理するよ
  30. • 基本的に class UserFactory extends BaseFactory というかんじで読み込む 具体的にはこんな感じに使えるよん <?php declare(strict_types=1);

    namespace Test\Factory; use CakephpFixtureFactories\Factory\BaseFactory; use Faker\Generator; class UserFactory extends BaseFactory { protected function getRootTableRegistryName(): string { return 'Users'; } protected function setDefaultTemplate(): void { $this->setDefaultData(function (Generator $faker) { return [ 'screen_name' => $faker->userName(), 'name' => $faker->name(), 'icon_url' => $faker->imageUrl(128, 128), ]; }); } }
  31. • UserFactory::make() でModel Factoryがセットアップされる ◦ この時点では class UserFactory に定義されてる protected

    function setDefaultTemplate(): void の中で定義されているデータをセットアップす るだけ 具体的にはこんな感じに使えるよん public function test_getUserById(): void { // given $userFactory = UserFactory::make();
  32. • UserFactory::make($data) でプロパティに値を渡せます ◦ 無名関数も渡せます 具体的にはこんな感じに使えるよん public function test_getUserById(): void

    { // given $takoba = UserFactory::make(['name' => 'takoba'])->persist(); $somebody = UserFactory::make(function (UserFactory $factory, Faker\Generator $faker) { return ['screen_name' => $faker->userName()]; }, 3)->persist();
  33. public function test_getUserById(): void { // given $users = UserFactory::make(2)->persist();

    // 以下は同義 $fixture = [new User(['name' => 'John']), new User(['name' => 'Paul'])]; $UsersTable = TableRegistry::getTableLocator()->get('Users'); $users = $UsersTable->saveManyOrFail($fixture); • CakePHPだと UserFactory::make()->persist() とかってやると cakephp/orm の class Cake\ORM\Table で定義された class UsersTable extends Table でデータ が保存されて、 class Cake\ORM\Entity で定義されてる class User extends Entity のインスタンスがヒュッと手に入る! 具体的にはこんな感じに使えるよん
  34. public function test_getUserById(): void { // given $users = UserFactory::make(2)

    ->with('UserAddresses', UserAddressFactory::make(3)) ->persist(); • そして UserFactory::make()->with('UserAddresses', UserAddressFactory::make(3)) みたいなかんじで簡単にassociationsも表現で きる!!! 具体的にはこんな感じに使えるよん
  35. public function test_getUserById(): void { // given $users = UserFactory::make(2)

    ->listeningToModelEvents(‘Model.beforeMarshal’) ->getEntity(); • UserFactory::make()->listeningToModelEvents('Model.beforeMarshal') とする ことでModelのイベントを発火することができる ◦ 基本、CakePHP Fixture Factories側で一通りoffにしているので明示的に呼 び出す必要がある 具体的にはこんな感じに使えるよん
  36. public function test_getUserById(): void { // given $users = UserFactory::make(2)

    ->listeningToBehaviors(‘Sluggable’) ->getEntity(); • UserFactory::make()->listeningToBehaviors('Sluggable') とすることでModelの Behaviorを呼び出すことができる ◦ CakePHP Fixture Factories側ではTimestampBehaviorだけ有効にしてある ◦ 'TestFixtureGlobalBehaviors' というConfigに有効にしたいBehaviorを設定 できる 具体的にはこんな感じに使えるよん
  37. • CakePHP3以上をサポートしてる ◦ 基本的にCakePHP3で導入された cakephp/orm のTableクラスとEntityクラ スを前提に実装されてる ◦ CakePHP2はがんばったら使えるかもだけど恩恵はあまり受けられないか な...

    ▪ フレームワークの前提が namespaced じゃないので、どう読み込むか は課題だけどたぶん使えそうではあるけど公式にはサポートされてな いし、そもそもEntityの恩恵は得られない サポート範囲
  38. • vierge-noire/test-database-cleaner を使ってデータベースをcleanupするライ ブラリも提供してたりはする ◦ これはLaravelでも他のアプリでも使えて、なおかつ速いぜ!という売り ◦ ちなみにCakePHPアプリだと cakephp/migrationを用いる方法 でスキーマ

    を作成して FixtureStrategyによるrefleshをする方法 が推奨されてる • データを生成したりデータベースへ登録するにはcakephp/ormに準拠したTable クラスとEntityクラスが必要になる ◦ cf. cakephp-fixture-factories/no_cake_associations.md at main · vierge-noire/cakephp-fixture-factories ◦ 将来的には拡張ができるようになるかもしれないけど、現状は一通り EventCompilerとかを書かないと差し替えられないね... ほんとにCakePHPに関わらず使えるの...?
  39. • CakePHP 4.3.0から、 class Cake\TestSuite\Fixture\TestFixture をベースにし たテストフィクスチャのライフサイクル戦略を設定することができるように なった • 以下のようなクラスがプリセットで用意されている

    • class TruncateStrategy implements FixtureStrategyInterface ◦ テストケース実行後にテーブルをTRUNCATEする • class TransactionStrategy implements FixtureStrategyInterface ◦ BEGIN; でトランザクション張って、 tearDown() 時に ROLLBACK; • 先ほど紹介したテストフィクスチャの戦略を実現できる...? FYI: CakePHPのフィクスチャステートマネージャ
  40. • Test fixture - Wikipedia • Rubyのテスティングフレームワークの歴史(2014年版) - 2014-11-06 -

    クク ログ • test-unit - Ruby用単体テストフレームワーク • index at XUnitPatterns.com ◦ Inline Setup at XUnitPatterns.com ◦ Delegated Setup at XUnitPatterns.com ◦ Implicit Setup at XUnitPatterns.com ◦ Shared Fixture at XUnitPatterns.com ◦ Fresh Fixture at XUnitPatterns.com 参考文献
  41. • Amazon.co.jp: xUnit Test Patterns: Refactoring Test Code (Addison-Wesley Signature

    Series (Fowler)) (English Edition) 電子書籍: Meszaros, Gerard: 洋書 • abseil / Software Engineering at Google • Software Design 2022年3月号|技術評論社 • vierge-noire/cakephp-fixture-factories: CakePHP Fixture Factories • CakeFest 2020 - Fixture Factories Plugin - Juan Pablo Ramirez - YouTube • データベースアクセス & ORM - CakePHP 4.x Strawberry Cookbook • 現在時刻が関わるユニットテストから、テスト容易性設計を学ぶ - t-wadaのブ ログ • Google Slidesにソースコードを貼り付ける - zenn.dev ◦ 参考文献というよりSpecial Thanks 参考文献