TDD には、配置、動作、アサート (BDD で指定、いつ、その後) の 3 つのフェーズがあります。アサートフェーズには優れたツールサポートがあり、AssertJ、FEST-Assert、または Hamcrest に精通しているかもしれません。アレンジフェーズとは対照的です。テスト データの整理は多くの場合困難であり、テストの重要な部分がそれに費やされるのが一般的ですが、それをサポートするツールを指定するのは困難です。
Test Arranger は、テストに必要なクラスのインスタンスを配置することで、このギャップを満たそうとします。インスタンスには、テスト データの作成プロセスを簡素化する擬似ランダム値が入力されます。テスターは必要なオブジェクトのタイプを宣言するだけで、新しいインスタンスを取得します。特定のフィールドの擬似ランダム値が十分ではない場合は、このフィールドのみを手動で設定する必要があります。
Product product = Arranger . some ( Product . class );
product . setBrand ( "Ocado" );
< dependency >
< groupId >com.ocadotechnology.gembus groupId >
< artifactId >test-arranger artifactId >
< version >1.6.3 version >
dependency >
testImplementation ' com.ocadotechnology.gembus:test-arranger:1.6.3 '
Arranger クラスには、単純型の疑似ランダム値を生成するための静的メソッドがいくつかあります。それぞれに、Kotlin の呼び出しを簡単にするためのラッピング機能があります。考えられる呼び出しの一部を以下に示します。
ジャワ | コトリン | 結果 |
---|---|---|
Arranger.some(Product.class) | some | すべてのフィールドに値が入力された Product のインスタンス |
Arranger.some(Product.class, "brand") | some | ブランドフィールドの値のない Product のインスタンス |
Arranger.someSimplified(Category.class) | someSimplified | カテゴリのインスタンス、タイプ コレクションのフィールドのサイズは 1 に減少し、オブジェクト ツリーの深さは 3 に制限されます |
Arranger.someObjects(Product.class, 7) | someObjects | Product インスタンスのサイズ 7 のストリーム |
Arranger.someEmail() | someEmail() | メールアドレスを含む文字列 |
Arranger.someLong() | someLong() | Long 型の疑似乱数 |
Arranger.someFrom(listOfCategories) | someFrom(listOfCategories) | listOfCategory からのエントリ |
Arranger.someText() | someText() | マルコフ連鎖から生成された文字列。デフォルトでは、これは非常に単純なチェーンですが、別の定義を持つテスト クラスパスに他の「enMarkovChain」ファイルを置くことで再構成できます。英語コーパスでトレーニングされたものはここで見つけることができます。ファイル形式については、プロジェクト「enMarkovChain」ファイルに含まれているファイルを参照してください。 |
- | some | 「あまりランダムではない」に設定されたname を除くすべてのフィールドがランダムな値で埋められた Product のインスタンス。この構文を使用してオブジェクトのフィールドを必要な数だけ設定できますが、各オブジェクトは変更可能である必要があります。 |
完全にランダムなデータは、すべてのテスト ケースに適しているとは限りません。多くの場合、テストの目標にとって重要であり、特定の値が必要なフィールドが少なくとも 1 つあります。配置されたクラスが可変である場合、またはそれが Kotlin データクラスである場合、または変更されたコピーを作成する方法がある場合 (例: Lombok の @Builder(toBuilder = true))、利用可能なものをそのまま使用します。幸いなことに、たとえ調整できなくても、テスト・アレンジャーを使用することができます。 some()
メソッドとsomeObjects()
メソッドには、 Map
型のパラメーターを受け入れる専用バージョンがあります。このマップのキーはフィールド名を表し、対応するサプライヤーは、Test Arranger がこれらのフィールドに設定する値を提供します。たとえば、次のようになります。
Product product = Arranger . some ( Product . class , Map . of ( "name" , () -> value ));
デフォルトでは、ランダム値はフィールド タイプに従って生成されます。ランダム値は、クラスの不変条件と必ずしもうまく対応するとは限りません。フィールドの値に関するいくつかのルールに従ってエンティティを常に配置する必要がある場合は、カスタム アレンジャーを提供できます。
class ProductArranger extends CustomArranger < Product > {
@ Override
protected Product instance () {
Product product = enhancedRandom . nextObject ( Parent . class );
product . setPrice ( BigDecimal . valueOf ( Arranger . somePositiveLong ( 9_999L )));
return product ;
}
}
Product
のインスタンス化プロセスを制御するには、 instance()
メソッドをオーバーライドする必要があります。メソッド内では、必要に応じてProduct
のインスタンスを作成できます。具体的には、いくつかのランダムな値を生成する場合があります。便宜上、 CustomArranger
クラスにはenhancedRandom
フィールドがあります。指定された例では、すべてのフィールドが擬似ランダム値を持つProduct
のインスタンスを生成しますが、その後、価格をドメイン内で受け入れられる値に変更します。これは負ではなく、10,000 の数値よりも小さいです。
ProductArranger
、 Product
の新しいインスタンスが要求されるたびに、Arranger によって自動的に (リフレクションを使用して) 取得され、使用されます。 Arranger.some(Product.class)
のような直接呼び出しだけでなく、間接的な呼び出しも考慮します。 List
型のフィールドproducts
を持つクラスShop
があると仮定します。 Arranger.some(Shop.class)
呼び出すと、アレンジャーはProductArranger
を使用してShop.products
に保存されているすべての製品を作成します。
テスト アレンジャーの動作は、プロパティを使用して構成できます。 arranger.properties
ファイルを作成し、クラスパスのルート (通常はsrc/test/resources/
ディレクトリ) に保存すると、そのファイルが選択され、次のプロパティが適用されます。
arranger.root
カスタム アレンジャーはリフレクションを使用して取得されます。 CustomArranger
拡張するすべてのクラスはカスタム アレンジャーとみなされます。リフレクションは、デフォルトではcom.ocado
である特定のパッケージに焦点を当てています。それは必ずしもあなたにとって都合がよいとは限りません。ただし、 arranger.root=your_package
を使用すると、 your_package
に変更できます。汎用的なもの (たとえば、多くのライブラリのルート パッケージであるcom
だけ) を含むと、何百ものクラスをスキャンすることになり、かなりの時間がかかるため、パッケージをできるだけ具体的にするようにしてください。arranger.randomseed
デフォルトでは、基になる擬似乱数値ジェネレーターの初期化に常に同じシードが使用されます。結果として、後続の実行では同じ値が生成されます。実行全体でランダム性を実現するには、つまり常に他のランダムな値から開始するには、 arranger.randomseed=true
設定する必要があります。arranger.cache.enable
ランダムなインスタンスを配置するプロセスには時間がかかります。多数のインスタンスを作成し、それらを完全にランダムにする必要がない場合は、キャッシュを有効にするのがよいでしょう。有効にすると、キャッシュは各ランダム インスタンスへの参照を保存し、ある時点でテスト アレンジャーは新しいインスタンスの作成を停止し、代わりにキャッシュされたインスタンスを再利用します。デフォルトではキャッシュは無効になっています。arranger.overridedefaults
Test-arranger は、デフォルトのフィールド初期化を尊重します。つまり、空の文字列で初期化されたフィールドがある場合、test-arranger によって返されるインスタンスには、このフィールドに空の文字列が含まれます。特に、フィールドを空の値で初期化する規則がプロジェクトにある場合は、必ずしもこれがテストに必要なわけではありません。幸いなことに、test-arranger にデフォルトをランダムな値で上書きさせることができます。デフォルトの初期化をオーバーライドするには、 arranger.overridedefaults
true に設定します。arranger.maxRandomizationDepth
一部のテスト データ構造では、相互参照するオブジェクトの任意の長さのチェーンを生成できます。ただし、テスト ケースでこれらを効果的に使用するには、これらのチェーンの長さを制御することが重要です。デフォルトでは、Test-arranger はネストの深さの 4 番目のレベルで新しいオブジェクトの作成を停止します。このデフォルト設定がプロジェクトのテストケースに適合しない場合は、このパラメーターを使用して調整できます。テスト データとして使用できる Java レコードがあり、そのフィールドの 1 つまたは 2 つを変更する必要がある場合、copy メソッドを備えたData
クラスが解決策を提供します。これは、フィールドを直接変更する明確な方法がない不変レコードを扱う場合に特に便利です。
Data.copy
メソッドを使用すると、必要なフィールドを選択的に変更しながら、レコードの浅いコピーを作成できます。フィールド オーバーライドのマップを提供することにより、変更する必要があるフィールドとその新しい値を指定できます。 copy メソッドは、更新されたフィールド値を使用してレコードの新しいインスタンスを作成します。
このアプローチにより、新しいレコード オブジェクトを手動で作成してフィールドを個別に設定する必要がなくなり、既存のレコードからわずかに異なるテスト データを生成する便利な方法が提供されます。
全体として、Data クラスとその copy メソッドは、選択したフィールドが変更されたレコードの浅いコピーの作成を可能にし、不変のレコード タイプを操作する際の柔軟性と利便性を提供することで状況を救います。
Data . copy ( myRecord , Map . of ( "recordFieldName" , () -> "altered value" ));
ソフトウェア プロジェクトのテストを行っているときに、これ以上改善できないという印象を抱くことはほとんどありません。テスト データの整理の範囲内で、Test Arranger で改善しようとしている領域が 2 つあります。
テストは、作成者の意図、つまり、なぜテストが書かれたのか、どのような問題を検出する必要があるのかを知ると、はるかに理解しやすくなります。残念ながら、テストの配置 (指定された) セクションに次のようなステートメントが含まれることは、特別なことではありません。
Product product = Product . builder ()
. withName ( "Some name" )
. withBrand ( "Some brand" )
. withPrice ( new BigDecimal ( "12.99" ))
. withCategory ( "Water, Juice & Drinks / Juice / Fresh" )
...
. build ();
このようなコードを見ると、どの値がテストに関連しており、どの値が非 null 要件を満たすためだけに提供されているのかを判断するのは困難です。テストがブランドに関するものであれば、次のように書いてみてはいかがでしょうか。
Product product = Arranger . some ( Product . class );
product . setBrand ( "Some brand" );
ブランドが重要であることは明らかです。さらに一歩前進してみましょう。テスト全体は次のようになります。
//arrange
Product product = Arranger . some ( Product . class );
product . setBrand ( "Some brand" );
//act
Report actualReport = sut . createBrandReport ( Collections . singletonList ( product ))
//assert
assertThat ( actualReport . getBrand ). isEqualTo ( "Some brand" )
現在、レポートが「Some brand」ブランドに対して作成されたかどうかをテストしています。しかし、それが目標でしょうか?特定の製品が割り当てられている同じブランドに対してレポートが生成されることを期待する方が合理的です。したがって、テストしたいのは次のとおりです。
//arrange
Product product = Arranger . some ( Product . class );
//act
Report actualReport = sut . createBrandReport ( Collections . singletonList ( product ))
//assert
assertThat ( actualReport . getBrand ). isEqualTo ( product . getBrand ())
ブランド フィールドが変更可能で、 sut
変更されるのではないかと心配な場合は、act フェーズに入る前にその値を変数に保存し、後でその値をアサーションに使用できます。テストは長くなるが、意図は明らかだ。
ここで行ったことは、Gerard Meszaros の「xUnit テスト パターン: テスト コードのリファクタリング」で説明されている、生成された値とある程度の作成メソッド パターンの適用であることは注目に値します。
実稼働コードで小さな変更を加えた結果、数十ものテストでエラーが発生したことはありますか?アサーションの失敗を報告するものもあれば、コンパイルを拒否するものもあります。これは、あなたの無実のテストを攻撃したショットガン手術コードの匂いです。まあ、小さな変化によって引き起こされる巻き添え被害を制限するために、別の方法で設計できるほど無害ではないかもしれません。例を使用して分析してみましょう。ドメインに次のクラスがあるとします。
class TimeRange {
private LocalDateTime start ;
private long durationinMs ;
public TimeRange ( LocalDateTime start , long durationInMs ) {
...
そしてそれは多くの場所で使用されているということです。特にテストでは、Test Arranger を使用せずに、次のようなステートメントを使用します。 new TimeRange(LocalDateTime.now(), 3600_000L);
いくつかの重要な理由により、クラスを次のように変更する必要がある場合はどうなりますか?
class TimeRange {
private LocalDateTime start ;
private LocalDateTime end ;
public TimeRange ( LocalDateTime start , LocalDateTime end ) {
...
すべての依存テストを中断することなく古いバージョンを新しいバージョンに変換する一連のリファクタリングを考え出すのは非常に困難です。より可能性が高いのは、クラスの新しい API に合わせてテストが 1 つずつ調整されるシナリオです。これは、期間の望ましい値 (慎重に LocalDateTime 型のend
に変換する必要があるのか、それとも単なる便利なランダムな値なのか) に関する多くの疑問を伴う、あまり面白くない作業がたくさんあることを意味します。 Test Arranger があれば作業はずっと楽になるでしょう。 null ではないTimeRange
必要とするすべての場所にArranger.some(TimeRange.class)
がある場合、古いバージョンの TimeRange と同様に新しいバージョンのTimeRange
にも適しています。これにより、ランダムではないTimeRange
必要とするいくつかのケースが残りますが、すでに Test Arranger を使用してテストの意図を明らかにしているため、それぞれのケースでTimeRange
にどの値を使用する必要があるかが正確にわかります。
しかし、テストを改善するために私たちができることはそれだけではありません。おそらく、 TimeRange
インスタンスのいくつかのカテゴリ (過去の範囲、将来の範囲、現在アクティブな範囲など) を識別できると考えられます。 TimeRangeArranger
は、次のことを調整するのに最適な場所です。
class TimeRangeArranger extends CustomArranger < TimeRange > {
private final long MAX_DISTANCE = 999_999L ;
@ Override
protected TimeRange instance () {
LocalDateTime start = enhancedRandom . nextObject ( LocalDateTime . class );
LocalDateTime end = start . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , end );
}
public TimeRange fromPast () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime end = now . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( end . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE )), end );
}
public TimeRange fromFuture () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime start = now . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , start . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE )));
}
public TimeRange currentlyActive () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime start = now . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
LocalDateTime end = now . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , end );
}
}
このような作成方法は、事前に作成するのではなく、既存のテスト ケースに対応させる必要があります。それにもかかわらず、 TimeRangeArranger
、テスト用にTimeRange
のインスタンスが作成されるすべてのケースをカバーする可能性があります。その結果、いくつかの不可解なパラメーターを伴うコンストラクター呼び出しの代わりに、作成されたオブジェクトのドメインの意味を説明し、テストの意図を理解するのに役立つ適切な名前のメソッドを備えたアレンジャーが用意されています。
Test Arranger によって解決される課題について議論する際に、テスト データ作成者の 2 つのレベルを特定しました。画像を完成させるには、少なくとももう 1 つ、つまりフィクスチャについて言及する必要があります。この説明では、Fixture がテスト データの複雑な構造を作成するために設計されたクラスであると仮定します。カスタム アレンジャーは常に 1 つのクラスに焦点を当てていますが、テスト ケース内で 2 つ以上のクラスのコンスタレーションが繰り返し発生することが観察される場合があります。それはユーザーとその銀行口座である可能性があります。それぞれに CustomArranger がある可能性がありますが、それらが一緒に使用されることが多いという事実を無視する必要はありません。この時点でフィクスチャについて考え始める必要があります。ユーザーと銀行口座の両方を作成し (おそらく専用のカスタム アレンジャーを使用して)、それらをリンクする責任があります。フィクスチャは、Gerard Meszaros による「xUnit テスト パターン: リファクタリング テスト コード」でいくつかの実装バリアントを含めて詳細に説明されています。
したがって、テスト クラスには 3 種類のビルディング ブロックがあります。それぞれは、実稼働コードの概念 (ドメイン駆動設計ビルディング ブロック) に相当すると考えることができます。
表面にはプリミティブで単純なオブジェクトがあります。それは最も単純な単体テストでも現れます。このようなテスト データの配置は、 Arranger
クラスのsomeXxx
メソッドを使用して行うことができます。
そのため、 User
インスタンス、またはUser
クラスと、アドレスのリストなど、 User
クラスに含まれる他のクラスの両方でのみ機能するテストを必要とするサービスが存在する場合があります。このようなケースに対処するには、通常、カスタム アレンジャー、つまりUserArranger
が必要です。すべての制約とクラスの不変条件を尊重してUser
のインスタンスを作成します。さらに、 AddressArranger
が存在する場合はそれを選択し、アドレスのリストに有効なデータを入力します。いくつかのテスト ケースで特定のタイプのユーザー (アドレスのリストが空のホームレス ユーザーなど) が必要な場合、UserArranger で追加のメソッドを作成できます。その結果、テスト用のUser
インスタンスを作成する必要がある場合は、 UserArranger
を調べて適切なファクトリ メソッドを選択するか、単にArranger.some(User.class)
を呼び出すだけで十分です。
最も困難なケースは、大規模なデータ構造に依存するテストに関するものです。 e コマースでは、多くの商品を含むショップだけでなく、ショッピング履歴のあるユーザー アカウントも含まれる可能性があります。このようなテスト ケースのデータを整理することは通常、簡単ではなく、そのようなことを繰り返すのは賢明ではありません。これをshopWithNineProductsAndFourCustomers
などの適切な名前のメソッドの下の専用クラスに保存し、各テストで再利用する方がはるかに優れています。このようなクラスには、見つけやすいように命名規則を使用することを強くお勧めします。私たちの提案は、 Fixture
postfix を使用することです。最終的には次のような結果になるかもしれません。
class ShopFixture {
Repository repo ;
public void shopWithNineProductsAndFourCustomers () {
Arranger . someObjects ( Product . class , 9 )
. forEach ( p -> repo . save ( p ));
Arranger . someObjects ( Customer . class , 4 )
. forEach ( p -> repo . save ( p ));
}
}
最新のテスト アレンジャー バージョンは Java 17 を使用してコンパイルされているため、Java 17 以降のランタイムで使用する必要があります。ただし、下位互換性を目的とした Java 8 ブランチもあり、1.4.x バージョンでカバーされています。