ホームページ >バックエンド開発 >PHPチュートリアル >すべてのビジネス ロジックを 1 秒未満でテストします
この記事はテストに関するものではありません。それは、機能の開発中に制御を維持できるワークフローを導入することです。テストは単なるエンジンであり、このプロセスの嬉しい結果です。
このワークフローは私のコーディング方法を完全に変え、常に私の顔に笑顔を与えてくれます。私の願いは、あなたにも同じことが起こることです。最終的には、すべてのビジネス ルールを満たす完全に開発された機能と、それを 1 秒以内に検証するテスト スイートが完成します。
デモには PHP を使用しましたが、このワークフローはどの言語にも完全に適応できます。
新しい機能を開発するときは、どこから始めればよいのか分からないことがよくあります。ビジネス ロジック、コントローラー、またはフロントエンドのどれから始めるべきですか?
いつやめるべきかを知るのも同様に難しい。明確なプロセスがなければ、進捗状況を評価する唯一の方法は手動テストです。退屈で間違いが発生しやすいアプローチです。
あなたはそれを知っています。説明不能なことが起こるあのファイル。たった 1 行を変更する必要がありますが、変更を試みるたびにプロジェクト全体が中断されてしまうようです。テストというセーフティネットがなければ、リファクタリングは渓谷の上を綱渡りしているような気分になります。
この混乱を避けるために、すべての機能をエンドツーエンド テストでテストすることにしました。素晴らしいアイデアですね!テストスイートが終了するまでコーヒーを 5 杯飲むのに十分な時間があることに気づくまでは。生産性?窓の外。
即座にフィードバックを作成するために、すべてのクラスに対して詳細なテストを作成することにしました。しかし、現在では、変更を加えるたびに一連の壊れたテストが発生し、実際の変更に取り組むよりもテストの修正に多くの時間を費やすことになります。それはイライラさせられ、非効率的で、すべてのリファクタリングが怖くなります。
フィードバックはソフトウェア開発のあらゆる段階で重要です。プロジェクトの中核を開発しているときに、ビジネス ルールに違反しているかどうかをすぐに知るために、即時フィードバックが必要です。リファクタリング中に、コードが壊れているかどうか、または安全に作業を進めることができるかどうかを教えてくれるアシスタントがいることは、非常に貴重な利点です。
プロジェクトで何ができるか、つまりプロジェクトの動作が最も重要な側面です。これらの動作はユーザー中心である必要があります。実装 (機能を動作させるために作成されたコード) が変わっても、ユーザーの意図は変わりません。
たとえば、ユーザーが商品を注文したときに通知を受け取りたいと考えています。ユーザーに通知するには、電子メール、SMS、郵便など、さまざまな方法があります。ユーザーの意図は変わりませんが、実装によって変わることがよくあります。
テストがユーザーの意図を表しており、コードがその意図を満たさなくなった場合、テストは失敗するはずです。ただし、リファクタリング中に、コードが依然としてユーザーの意図を満たしている場合、テストは中断されるべきではありません。
新しい機能の作成を段階的にガイドされるところを想像してみてください。最初にテストを作成すると、それが正しい機能をコーディングするためのガイドになります。まだ完全には明らかではないかもしれませんが、コードを作成している間、テストを GPS として機能させたいと考えています。このアプローチでは、次のステップについて考える必要はなく、GPS の指示に従うだけです。この概念がまだ明確ではないと感じても、心配しないでください。ワークフローのセクションで詳しく説明します。 ?
冒頭で述べたように、この記事はテストに関するものではなく、機能の作成に関するものです。そのためには機能が必要です。より具体的には、例で定義された受け入れテストを伴う機能が必要です。チームは機能ごとに受け入れ基準またはルールを確立し、ルールごとに 1 つ以上の例を作成する必要があります。
⚠️ これらの例はユーザー/ドメイン中心である必要があり、技術的な側面は説明されていません。サンプルが適切に定義されているかどうかを確認する簡単な方法は、次のように自問することです。「私のサンプルは、どのような実装 (Web フロントエンド、HTTP API、ターミナル CLI、現実世界など) でも機能しますか?」
たとえば、「商品をバスケットに追加」機能を開発したい場合、例は次のようになります:
これらの例はテストの基礎として機能します。より複雑なシナリオでは、詳細を追加するために Given/When/Then パターンを使用しますが、単純なシナリオでは必要ありません。
即時のフィードバックと行動への注目、これはかなりの挑戦ではないでしょうか? Symfony MVC サービス アーキテクチャでは、これを達成するのは難しい場合があります。そのため、私はドメイン中心のアーキテクチャを使用します。これについては、この記事「Symfony プロジェクトを構造化する別の方法」で詳しく説明しました。
動作に焦点を当てるには、テストを正しい方法で構成する必要があります。まず、ユーザーのアクション前のシステムの状態を説明します。次に、システムの状態を変更するユーザー アクションを実行します。最後に、システムの状態は私の期待どおりであると断言します。
この方法でテストすることにより、テストではユーザーのアクションがシステムによってどのように処理されるかを気にする必要がなくなります。アクションが成功したかどうかのみをチェックします。これは、ユーザー アクションの実装を変更してもテストは中断されないことを意味します。ただし、実装がシステムの状態を期待どおりに更新できない場合、テストは中断されます。それがまさに私たちが望んでいることです。
即時のフィードバックを実現するには、システムの特定の部分をモックする必要があります。ドメイン中心のアーキテクチャは、ドメインが外部ライブラリと対話するインターフェースのみに依存しているため、これを可能にします。これにより、これらの依存関係を偽装することが驚くほど簡単になり、機能テストを超高速に実行できるようになります。もちろん、実際の実装も (この記事ではありませんが) 統合テストを使用してテストされます。
このワークフローでは、テストは私の GPS です!目的地を設定し、ガイドに従って、到着したら通知します。テストは、チームが提供する例に基づいて具体化されます。
ビジネス ロジックとユーザーの意図をテストするために、機能テストを使用します。
// tests/Functional/AddProductToBasketTest.php namespace App\Tests\Functional; use PHPUnit\Framework\TestCase; class AddProductToBasketTest extends TestCase { public function testAddProductToBasket() { } }
このファイルにはこの機能のすべてのテストが含まれていますが、最初のテストから始めましょう。
最初の部分では、アプリケーションの状態について説明します。ここには、ID「1」の顧客用の空のバスケットがあります。
$customerId = 1; $productId = 1; $basketId = 1; $basket = new Basket($basketId, customerId: $customerId); $inMemoryBasketRepository = new InMemoryBasketRepository(baskets: [$basket]);
ハンドラーとコマンドはユーザーの意図を表します。このように明示的であり、誰もが理解できます。
$commandHandler = new AddProductToBasketCommandHandler( $inMemoryBasketRepository, ); $commandHandler(new AddProductToBasketCommand(customerId: $customerId, basketId: $basketId, productId: $productId));
? : このプロジェクトではコマンドとクエリを分離することにしましたが、AddProductToBasketUseCase を使用することもできます。
最後に、最終結果がどのようになるかを説明します。バスケットに適切な商品が入っていることを期待しています。
$expectedBasket = new Basket($basketId, $customerId, array($productId)); $this->assertEquals($expectedBasket, $inMemoryBasketRepository->baskets[0]);
これは、最初の例をコードに変換するテストです。
// tests/Functional/AddProductToBasketTest.php namespace App\Tests\Functional; use PHPUnit\Framework\TestCase; class AddProductToBasketTest extends TestCase { public function testAddProductToBasket(): void { // Arrange $customerId = 1; $productId = 1; $basketId = 1; $basket = new Basket($basketId, customerId: $customerId); $inMemoryBasketRepository = new InMemoryBasketRepository(baskets: [$basket]); // Act $commandHandler = new AddProductToBasketCommandHandler( $inMemoryBasketRepository, ); $commandHandler(new AddProductToBasketCommand(customerId: $customerId, basketId: $basketId, productId: $productId)); // Assert $expectedBasket = new Basket($basketId, $customerId, array($productId)); $this->assertEquals($expectedBasket, $inMemoryBasketRepository->baskets[0]); } }
この時点では、このテストで使用したものがまだ存在しないため、IDE のあらゆる場所でエラーが表示されます。しかし、これらは本当に間違いなのでしょうか、それとも単に目的地に向かうための次のステップなのでしょうか?私はこれらのエラーを指示として扱い、テストを実行するたびに次に何をすべきかを指示します。そうすれば、考えすぎる必要がなくなります。私は GPS に従っているだけです。
? ヒント: 開発者のエクスペリエンスを向上させるために、変更を加えるたびにテスト スイートを自動的に実行するファイル ウォッチャーを有効にすることができます。テスト スイートは非常に高速であるため、更新のたびにすぐにフィードバックを受け取ることができます。
この例では、各エラーに 1 つずつ対処します。もちろん、自信がある場合は、途中でショートカットをすることもできます。 ?
それでは、テストを実行しましょう!エラーごとに、次のステップに進むために必要な最低限の作業を行います。
❌ ? : 「エラー : クラス "AppTestsFunctionalBasket" が見つかりません」
この機能の最初のテストであるため、ほとんどのクラスはまだ存在していません。最初のステップとして、Basket オブジェクトを作成します:
// tests/Functional/AddProductToBasketTest.php namespace App\Tests\Functional; use PHPUnit\Framework\TestCase; class AddProductToBasketTest extends TestCase { public function testAddProductToBasket() { } }
❌ ? : 「エラー : クラス "AppTestsFunctionalInMemoryBasketRepository" が見つかりません」
InMemoryBasketRepository を作成します:
$customerId = 1; $productId = 1; $basketId = 1; $basket = new Basket($basketId, customerId: $customerId); $inMemoryBasketRepository = new InMemoryBasketRepository(baskets: [$basket]);
❌ ? : 「エラー : クラス "AppTestsFunctionalAddProductToBasketCommandHandler" が見つかりません」
カタログ/アプリケーション フォルダーに AddProductToBasketCommandHandler を作成します。このハンドラーは、「商品をバスケットに追加」機能のエントリ ポイントになります。
$commandHandler = new AddProductToBasketCommandHandler( $inMemoryBasketRepository, ); $commandHandler(new AddProductToBasketCommand(customerId: $customerId, basketId: $basketId, productId: $productId));
ドメイン中心のアーキテクチャを尊重するために、このように依存関係を逆転させて、リポジトリのインターフェイスを作成します。
$expectedBasket = new Basket($basketId, $customerId, array($productId)); $this->assertEquals($expectedBasket, $inMemoryBasketRepository->baskets[0]);
❌ ? : "TypeError : AppCatalogApplicationCommandAddProductToBasketAddProductToBasketCommandHandler::__construct(): 引数 #1 ($basketRepository) のタイプは AppCatalogDomainBasketRepository、AppCatalogInfra StructurePersistenceInMemoryInMemoryBasketRepository である必要があります与えられた"
ここで、InMemory 実装は、コマンド ハンドラーに挿入できるようにインターフェイスを実装する必要があります。
// tests/Functional/AddProductToBasketTest.php namespace App\Tests\Functional; use PHPUnit\Framework\TestCase; class AddProductToBasketTest extends TestCase { public function testAddProductToBasket(): void { // Arrange $customerId = 1; $productId = 1; $basketId = 1; $basket = new Basket($basketId, customerId: $customerId); $inMemoryBasketRepository = new InMemoryBasketRepository(baskets: [$basket]); // Act $commandHandler = new AddProductToBasketCommandHandler( $inMemoryBasketRepository, ); $commandHandler(new AddProductToBasketCommand(customerId: $customerId, basketId: $basketId, productId: $productId)); // Assert $expectedBasket = new Basket($basketId, $customerId, array($productId)); $this->assertEquals($expectedBasket, $inMemoryBasketRepository->baskets[0]); } }
❌ ? : _"エラー: クラス "AppTestsFunctionalAddProductToBasketCommand" が見つかりません"
_
コマンドを作成します。コマンドは常に DTO であるため、読み取り専用としてマークし、すべてのプロパティをパブリックに設定しました。
// src/Catalog/Domain/Basket.php namespace App\Catalog\Domain; class Basket { public function __construct( public readonly string $id, public readonly string $customerId, public array $products = [], ) { } }
❌ ? : 「2 つのオブジェクトが等しいというアサートに失敗しました。」
テストをコンパイル中です!まだ赤いのでまだ終わりではありません、続けましょう! ?
ビジネス ロジックをコーディングします。テストを書いたときと同じように、まだ何も存在していませんが、ハンドラーがコマンドをどのように処理するかを予想します。コンパイルはできませんが、もう一度テストによって次のステップに進むことができます。
// src/Catalog/Infrastructure/Persistence/InMemory/InMemoryBasketRepository.php namespace App\Catalog\Infrastructure\Persistence\InMemory; class InMemoryBasketRepository { public function __construct( public array $baskets ) { } }
❌ ? : 「エラー: 未定義のメソッド AppCatalogInfra StructurePersistenceInMemoryInMemoryBasketRepository::get() の呼び出し」
リポジトリインターフェースに get メソッドを作成します:
// tests/Functional/AddProductToBasketTest.php namespace App\Tests\Functional; use PHPUnit\Framework\TestCase; class AddProductToBasketTest extends TestCase { public function testAddProductToBasket() { } }
❌ ? : 「PHP 致命的エラー: クラス AppCatalogInfrastructorPersistenceInMemoryInMemoryBasketRepository には 1 つの抽象メソッドが含まれているため、抽象メソッドを宣言するか、残りのメソッド (AppCatalogDomainBasketRepository::get) を実装する必要があります。」
そして今、それを InMemory リポジトリに実装します。テストするだけなので、非常に単純な実装を作成します:
$customerId = 1; $productId = 1; $basketId = 1; $basket = new Basket($basketId, customerId: $customerId); $inMemoryBasketRepository = new InMemoryBasketRepository(baskets: [$basket]);
❌ ? : 「エラー: 未定義のメソッド AppCatalogDomainBasket::add() の呼び出し」
add メソッドを作成し、バスケット オブジェクトに実装します
$commandHandler = new AddProductToBasketCommandHandler( $inMemoryBasketRepository, ); $commandHandler(new AddProductToBasketCommand(customerId: $customerId, basketId: $basketId, productId: $productId));
❌ ? : エラー: 未定義のメソッド AppCatalogInfra StructurePersistenceInMemoryInMemoryBasketRepository::save()
の呼び出し
ここでも同様に、インターフェースでメソッドを作成し、それをメモリ内リポジトリに実装します。
$expectedBasket = new Basket($basketId, $customerId, array($productId)); $this->assertEquals($expectedBasket, $inMemoryBasketRepository->baskets[0]);
// tests/Functional/AddProductToBasketTest.php namespace App\Tests\Functional; use PHPUnit\Framework\TestCase; class AddProductToBasketTest extends TestCase { public function testAddProductToBasket(): void { // Arrange $customerId = 1; $productId = 1; $basketId = 1; $basket = new Basket($basketId, customerId: $customerId); $inMemoryBasketRepository = new InMemoryBasketRepository(baskets: [$basket]); // Act $commandHandler = new AddProductToBasketCommandHandler( $inMemoryBasketRepository, ); $commandHandler(new AddProductToBasketCommand(customerId: $customerId, basketId: $basketId, productId: $productId)); // Assert $expectedBasket = new Basket($basketId, $customerId, array($productId)); $this->assertEquals($expectedBasket, $inMemoryBasketRepository->baskets[0]); } }
✅ ? :
目的地に到着しました! ? ? ?
このワークフローでは、開発者のエクスペリエンスをゲーム化しました。赤のテストに挑戦した後、緑に変わるのを見るとドーパミンが分泌されます ??
テストに合格するために、私は意図的に少し速く走った。これで、時間をかけてテストを改良し、より良い方法でコードを実装できるようになりました。
テストは例を翻訳しましたが、翻訳中にユーザーの意図を失いました。関数を少なくすることで、元の例にかなり近づけることができます。
例:
テスト:
// src/Catalog/Domain/Basket.php namespace App\Catalog\Domain; class Basket { public function __construct( public readonly string $id, public readonly string $customerId, public array $products = [], ) { } }
これはこの記事の焦点ではないため、詳細は説明しませんが、リッチ ドメイン モデルを使用すると、ドメイン モデルをより正確に表現できます。
テストがグリーンになったので、好きなだけリファクタリングできます。テストは私を守ってくれます。 ?
記事の冒頭で見たように、1 つの機能にはそれを説明するための多くの例があります。それでは、それらをすべて実装するときが来ました:
// src/Catalog/Infrastructure/Persistence/InMemory/InMemoryBasketRepository.php namespace App\Catalog\Infrastructure\Persistence\InMemory; class InMemoryBasketRepository { public function __construct( public array $baskets ) { } }
このワークフローをさまざまなテストで何度も実行し、コードのさまざまな部分がビジネス ルールに合わせて進化しました。最初のテストで見たように、私のオブジェクトは非常に単純で、場合によっては基本的なものでさえありました。しかし、新しいビジネス ルールが導入されると、よりスマートなドメイン モデルを開発するよう私に迫られました。私のフォルダー構造は次のとおりです:
ドメイン モデルのカプセル化を維持しながらテストを簡素化するために、ビルダー パターンとスナップショット パターンを導入しました。
// tests/Functional/AddProductToBasketTest.php namespace App\Tests\Functional; use PHPUnit\Framework\TestCase; class AddProductToBasketTest extends TestCase { public function testAddProductToBasket() { } }
$customerId = 1; $productId = 1; $basketId = 1; $basket = new Basket($basketId, customerId: $customerId); $inMemoryBasketRepository = new InMemoryBasketRepository(baskets: [$basket]);
$commandHandler = new AddProductToBasketCommandHandler( $inMemoryBasketRepository, ); $commandHandler(new AddProductToBasketCommand(customerId: $customerId, basketId: $basketId, productId: $productId));
使用方法:
$expectedBasket = new Basket($basketId, $customerId, array($productId)); $this->assertEquals($expectedBasket, $inMemoryBasketRepository->baskets[0]);
私はビジネス ロジックに完全に集中しているので、テストはすべてのステップで私をガイドし、新しいコードを追加するたびに何をすべきかを教えてくれます。さらに、すべてが機能することを確認するために手動テストを実行する必要がなくなり、効率が大幅に向上しました。
この記事では、主にクラスを作成する非常に単純な例を示しました。ただし、ビジネス ロジックがより複雑になると、新しいルールが追加されるたびにドメイン モデルが複雑になります。リファクタリング中の私の目標は、テストをグリーンに保ちながらドメイン モデルを簡素化することです。これは本当に難しい課題ですが、最終的にうまくいくと信じられないほど満足のいくものです。
ここで示したのは、完全なワークフローの最初の部分にすぎません。前述したように、完全に機能するためには、実際のアダプターを実装する必要があります。この場合、実際のデータベースを使用したテストファーストのアプローチを使用してリポジトリ (バスケット、顧客、製品) を作成することが含まれます。それが完了したら、依存関係注入構成で実際の実装を構成するだけです。
フレームワークに依存せずに純粋な PHP を使用し、すべてのビジネス ロジックをカプセル化することだけに重点を置きました。この戦略により、テストのパフォーマンスについて心配する必要がなくなりました。テストが数百、あるいは数千ある場合でも、テストはわずか数秒で実行され、コーディング中に即座にフィードバックが提供されます。
パフォーマンスのヒントを得るために、10,000 回繰り返されたテスト スイートを次に示します。
このワークフローは、いくつかの概念が交差する位置にあります。必要に応じてさらに詳しく調べられるように、その一部を紹介します。
機能を説明するには、必要なだけ多くの例を提供するのが最善の方法です。チームミーティングは、機能がどのように機能するかの具体例を特定することで、機能を深く理解するのに役立ちます。
Given-When-Then フレームワークは、これらの例を形式化するための優れたツールです。
これらの例をコードに変換するには、Gherkin 言語 (PHP の Behat を使用) が役立ちます。ただし、個人的には、PHPUnit テストで直接作業し、これらのキーワードを使用して関数に名前を付けることを好みます。
「私たちは何を望んでいますか?」この部分は頭字語 F.I.R.S.T で要約できます:
これで、テストが適切に行われたかどうかを確認するためのチェックリストが完成しました。
これらのアーキテクチャはすべて、ビジネス ロジックを実装の詳細から分離することを目的としています。ポートとアダプター、ヘキサゴナル、またはクリーン アーキテクチャのいずれを使用する場合でも、中心となるアイデアは、ビジネス ロジック フレームワークに依存せず、テストを容易にすることです。
これを念頭に置けば、あらゆる実装があり、最適なものはコンテキストと好みによって異なります。このアーキテクチャの主な利点は、ビジネス ロジックを分離することで、より効率的なテストが可能になることです。
テストのピラミッドについてはすでにご存知かと思いますが、代わりにダイヤモンドの表現を使用することを好みます。 Thomas Pierrain によって説明されたこの戦略は、ユースケース主導型であり、アプリケーションの動作に焦点を当てています。
この戦略は、アプリケーションのブラックボックス テストも奨励し、アプリケーションがどのように生成するかではなく、生成される結果に焦点を当てます。このアプローチにより、リファクタリングが大幅に容易になります。
ワークフロー全体を通して見たように、テストやコマンド ハンドラーでは、私は常に、必要なものが存在する前に書きました。いくつかの「願い」を叶えました。このアプローチは希望的思考プログラミングとして知られています。これにより、実装方法を決定する前に、システムの構造とそれと対話する理想的な方法を想像することができます。テストと組み合わせると、プログラミングをガイドする強力な方法になります。
このパターンは、構造テストを効果的に行うのに役立ちます。まず、システムが正しい初期状態に設定されます。次に、コマンドやユーザーアクションなどの状態修飾子が実行されます。最後に、アクション後にシステムが期待どおりの状態になるようにアサーションが行われます。
ドメイン中心のアーキテクチャと受け入れテスト駆動の開発を組み合わせることで、開発エクスペリエンスがどのように変化するのかを説明してきました。即時のフィードバック、堅牢なテスト、ユーザーの意図への焦点により、コーディングがより効率的になるだけでなく、より楽しくなります。
このワークフローを試してみると、テストが緑になるたびに笑顔になるかもしれません ✅ ➡️ ?
あなたの完璧なワークフロー、またはこのワークフローのどこを改善したいか、コメント欄で教えてください。
以上がすべてのビジネス ロジックを 1 秒未満でテストしますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。