09. 工場のテスト – Stitcher.io

in Vlog

(jp) =

<!–

–>

注: この章では、主にドメイン関連のコードについて説明します。 今後の章でアプリケーション層に戻ります。

CRUDを超えたLaravelのこの章では、テスト用のドメインデータを管理する方法を見ていきます。 Laravel のテスト ファクトリはよく知られている概念ですが、多くの点で欠けています。それらはあまり柔軟ではなく、ユーザーにとって一種のブラック ボックスでもあります。

ファクトリ状態の例を見てみましょう。これは強力なパターンですが、Laravel では十分に実装されていません。

$factory->state(Invoice::class, 'pending', [
    'status' => PaidInvoiceState::class,
]);

まず第一に、あなたの IDE はどんな種類のオブジェクトか見当もつかない $factory 実際にあります。 オートコンプリートはありませんが、ファクトリ ファイルに魔法のように存在します。 手っ取り早い解決策は、この docblock を追加することですが、これは面倒です。


$factory->state();

次に、ステートは文字列として定義されるため、実際にテストでファクトリを使用する場合はブラック ボックスになります。

public function test_case()

    $invoice = factory(Invoice::class)
        ->states()
        ->create();

第 3 に、ファクトリの結果には型ヒントがなく、IDE はそれを知りません。 $invoice 実際には Invoice モデル; 繰り返しますが、ブラック ボックスです。

そして最後に、ドメインが十分に大きい場合、テスト スイートに複数の状態が必要になる可能性があり、時間の経過とともに管理が難しくなります。

この章では、このファクトリ パターンを実装する別の方法を見て、柔軟性を大幅に高め、ユーザー エクスペリエンスを大幅に向上させます。 これらのファクトリ クラスの実際の目標は、システムのセットアップにあまり時間を費やすことなく、統合テストを作成できるようにすることです。

「単体テスト」ではなく「統合テスト」と言っていることに注意してください。ドメイン コードをテストしているときは、コア ビジネス ロジックをテストしていることになります。 多くの場合、このビジネス ロジックをテストするということは、クラスの孤立した部分をテストするのではなく、データベースに存在するデータの一部 (または大量) を必要とする複雑で入り組んだビジネス ルールをテストすることを意味します。

前に述べたように、この本では大規模で複雑なシステムについて話しています。 それを心に留めておくことが重要です。 特に、それが私がこれらのテストを呼び出すことにした理由です 統合 この章のテスト。 これは、単体テストとは何か、そうでないものについての議論を避けるためでした。

# 基本的な工場

テスト ファクトリは単純なクラスにすぎません。 必要なパッケージも、実装するインターフェースも、拡張する抽象クラスもありません。 ファクトリの力はコードの複雑さではなく、適切に適用された 1 つまたは 2 つのパターンにあります。

このようなクラスを単純化すると、次のようになります。

class InvoiceFactory

    public static function new(): self
    
        return new self();
    
    
    public function create(array $extra = []): Invoice
    
        return Invoice::create(array_merge(
            [
                'number' => 'I-1',
                'status' => PendingInvoiceState::class,
                
            ],
            $extra
        ));   
    

いくつかの設計上の決定について説明しましょう。

まず、静的コンストラクター new. なぜそれが必要なのか混乱するかもしれません。 create メソッド静的。 この質問については、この章の後半で詳しく説明しますが、現時点では、実際に請求書を作成する前に、このファクトリを高度に構成できるようにする必要があることを知っておく必要があります。 安心してください、すぐに明らかになります。

第二に、名前の理由 new 静的コンストラクターの? 答えは実用的なものです。工場のコンテキスト内では、 makecreate 多くの場合、実際に結果を生成する工場に関連付けられています。 new 不必要な混乱を避けるのに役立ちます。

最後に、 create メソッド: テストの最終段階でいつでも変更できるようにするために、オプションの追加データの配列が必要です。

簡単な例では、次のように請求書を作成できます。

public function test_case()

    $invoice = InvoiceFactory::new()->create();

構成可能性を検討する前に、すぐにできる改善点について説明しましょう。請求書番号は一意である必要があるため、1 つのテスト ケースで 2 つの請求書を作成すると壊れます。 ただし、ほとんどの場合、請求書番号を追跡することについて心配したくないので、工場にそれらの面倒を見てもらいましょう。

class InvoiceFactory

    private static int $number = 0;

    public function create(array $extra = []): Invoice
    
        self::$number += 1;

        return Invoice::create(array_merge(
            [
                'number' => 'I-' . self::$number,
                
            ],
            $extra
        ));   
    

# 工場の中の工場

元の例では、支払い済みの請求書を作成する必要があることを示しました。 これは単に請求書モデルのステータス フィールドを変更することを意味すると思っていたとき、私は以前は少し素朴でした。 また、実際の支払いをデータベースに保存する必要があります。 Laravel のデフォルト ファクトリは、モデルが作成された後にトリガーされるコールバックでこれを処理できます。 ただし、それぞれ独自の副作用を持ついくつかの、場合によっては数十の状態を管理している場合にどうなるか想像してみてください。 シンプルな $factory->afterCreating フックは、これらすべてを適切な方法で管理できるほど堅牢ではありません。

それでは、物事を好転させましょう。 請求書ファクトリを適切に構成しましょう。 実際の請求書を作成します。

class InvoiceFactory

    private string $status = null;

    public function create(array $extra = []): Invoice
    
        $invoice = Invoice::create(array_merge(
            [
                'status' => $this->status ?? PendingInvoiceState::class
            ],
            $extra
        ));
        
        if ($invoice->status->isPaid()) 
            PaymentFactory::new()->forInvoice($invoice)->create();
        
        
        return $invoice;
    

    public function paid(): self
    
        $clone = clone $this;
        
        $clone->status = PaidInvoiceState::class;
        
        return $clone;
    

それが気になるなら clone ちなみに、後で見てみましょう。

構成可能にしたのは、Laravel の工場状態と同じように、請求書のステータスですが、私たちの場合、IDE が何を扱っているかを実際に認識しているという利点があります。

public function test_case()

    $invoice = InvoiceFactory::new()
        ->paid()
        ->create();

それでも、改善の余地があります。 請求書が作成された後に私たちが行うチェックを見たことがありますか?

if ($invoice->status->isPaid()) 
    PaymentFactory::new()->forInvoice($invoice)->create();

これはさらに柔軟にすることができます。 を使用しています PaymentFactory しかし、その支払いがどのように行われたかについて、よりきめ細かい制御が必要な場合はどうでしょうか? たとえば、支払いの種類に応じて異なる動作をする、支払い済みの請求書に関するいくつかのビジネス ルールがあることを想像できます。

また、あまりにも多くの設定を直接 InvoiceFactory、 すぐにめちゃくちゃになるからです。 では、これをどのように解決しますか?

答えは次のとおりです。開発者がオプションで PaymentFactoryInvoiceFactory 開発者が望むようにこのファクトリを構成できるようにします。 これがどのように見えるかです:

public function paid(PaymentFactory $paymentFactory = null): self

    $clone = clone $this;
    
    $clone->status = PaidInvoiceState::class;
    $clone->paymentFactory = $paymentFactory ?? PaymentFactory::new();
    
    return $clone;

そして、ここでそれがどのように使用されているかです create 方法:

if ($this->paymentFactory) 
    $this->paymentFactory->forInvoice($invoice)->create();

そうすることで、多くの可能性が生まれます。 この例では、特に Bancontact 支払いで支払われる請求書を作成しています。

public function test_case()

    $invoice = InvoiceFactory::new()
        ->paid(
            PaymentFactory::new()->type(BancontactPaymentType::class)
        )
        ->create();

別の例: 請求書が支払われたときにどのように処理されるかをテストしたいが、請求書の有効期限が切れた後のみ:

public function test_case()

    $invoice = InvoiceFactory::new()
        ->expiresAt('2020-01-01')
        ->paid(
            PaymentFactory::new()->at('2020-01-20')
        )
        ->create();

ほんの数行のコードで、柔軟性が大幅に向上します。

# 不変の工場

では、以前のクローンについてはどうでしょうか。 ファクトリを不変にすることが重要なのはなぜですか? 同じ工場でいくつかのモデルを作成する必要がある場合がありますが、わずかな違いがあります。 モデルごとに新しいファクトリ オブジェクトを作成する代わりに、元のファクトリ オブジェクトを再利用して、必要なものだけを変更することができます。

ただし、不変のファクトリを使用していない場合は、実際には必要のないデータになってしまう可能性があります。 請求書の支払いの例を見てみましょう。同じ日に 2 つの請求書が必要で、1 つは支払い済みで、もう 1 つは保留中であるとします。

$invoiceFactory = InvoiceFactory::new()
    ->expiresAt(Carbon::make('2020-01-01'));

$invoiceA = $invoiceFactory->paid()->create();
$invoiceB = $invoiceFactory->create();

私たちの場合 paid メソッドは不変ではありませんでした。つまり、 $invoiceB また、支払われた請求書になります! 確かに、すべてのモデル作成を細かく管理することはできますが、それではこのパターンの柔軟性が失われます。 不変関数が優れているのはそのためです。意図しない副作用を心配することなく、ベース ファクトリをセットアップし、テスト全体で再利用できます。


これらの 2 つの原則 (ファクトリ内にファクトリを構成し、それらを不変にする) に基づいて、多くの可能性が生まれます。 確かに、これらのファクトリを実際に作成するには時間がかかりますが、 保存 開発の過程で多くの時間を費やします。 私の経験では、コストに比べて得られるものがはるかに多いため、オーバーヘッドに見合うだけの価値があります。

このパターンを使って以来、Laravel のビルトイン ファクトリを振り返ることはありませんでした。 このアプローチから得られるものは多すぎます。

私が思いつく欠点の 1 つは、一度に複数のモデルを作成するには、もう少し余分なコードが必要になることです。 ただし、必要に応じて、次のような基本ファクトリ クラスに小さなコードを簡単に追加できます。

abstract class Factory

    
    abstract public function create(array $extra = []);

    public function times(int $times, array $extra = []): Collection
    
        return collect()
            ->times($times)
            ->map(fn() => $this->create($extra));
    

また、これらのファクトリはモデルだけでなく、他のものにも使用できることに注意してください。 また、DTO をセットアップするためにそれらを広範囲に使用しており、時にはクラスを要求することさえあります。

次にテスト ファクトリが必要になったときに、それらを試してみることをお勧めします。 彼らが失望しないことを保証できます!

//platform.twitter.com/widgets.js

関連記事

前の投稿
Fire Emblem Engage はよりスリムなタクティカル JRPG のように感じます
次の投稿
Fortnite Winterfest が無料スキン、保管庫にない武器などでキックオフ