1. 概要

動的テストは、JUnit5で導入された新しいプログラミングモデルです。 この記事では、正確に動的なテストとは何か、およびそれらを作成する方法について説明します。

JUnit 5をまったく使用したことがない場合は、 JUnit5のプレビューとプライマリガイドを確認することをお勧めします。

2. DynamicTest とは何ですか?

@Test アノテーションが付けられた標準テストは、コンパイル時に完全に指定される静的テストです。 DynamicTestは、実行時に生成されるテストです。 これらのテストは、@TestFactoryアノテーションが付けられたファクトリメソッドによって生成されます。

@TestFactory メソッドは、DynamicTestのStream Collection Iterable 、またはIteratorを返す必要がありますインスタンス。 それ以外のものを返すと、コンパイル時に無効な戻りタイプを検出できないため、JUnitExceptionが発生します。 これとは別に、@TestFactoryメソッドをstaticまたはprivateにすることはできません。

DynamicTest は、標準の @Test とは異なる方法で実行され、ライフサイクルコールバックをサポートしていません。 つまり、DynamicTestsに対して@BeforeEachメソッドと@AfterEachメソッドは呼び出されません。

3. DynamicTestsの作成

まず、DynamicTestを作成するさまざまな方法を見てみましょう。

ここでの例は本質的に動的ではありませんが、真に動的な例を作成するための良い出発点を提供します。

DynamicTestコレクションを作成します。

@TestFactory
Collection<DynamicTest> dynamicTestsWithCollection() {
    return Arrays.asList(
      DynamicTest.dynamicTest("Add test",
        () -> assertEquals(2, Math.addExact(1, 1))),
      DynamicTest.dynamicTest("Multiply Test",
        () -> assertEquals(4, Math.multiplyExact(2, 2))));
}

@TestFactory メソッドは、これが動的テストを作成するためのファクトリであることをJUnitに通知します。 ご覧のとおり、DynamicTestCollectionのみを返しています。 各DynamicTestは、テスト名または表示名と実行可能ファイルの2つの部分で構成されます。

出力には、動的テストに渡した表示名が含まれます。

Add test(dynamicTestsWithCollection())
Multiply Test(dynamicTestsWithCollection())

同じテストを変更して、 Iterable Iterator 、またはStreamを返すことができます。

@TestFactory
Iterable<DynamicTest> dynamicTestsWithIterable() {
    return Arrays.asList(
      DynamicTest.dynamicTest("Add test",
        () -> assertEquals(2, Math.addExact(1, 1))),
      DynamicTest.dynamicTest("Multiply Test",
        () -> assertEquals(4, Math.multiplyExact(2, 2))));
}

@TestFactory
Iterator<DynamicTest> dynamicTestsWithIterator() {
    return Arrays.asList(
      DynamicTest.dynamicTest("Add test",
        () -> assertEquals(2, Math.addExact(1, 1))),
      DynamicTest.dynamicTest("Multiply Test",
        () -> assertEquals(4, Math.multiplyExact(2, 2))))
        .iterator();
}

@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
    return IntStream.iterate(0, n -> n + 2).limit(10)
      .mapToObj(n -> DynamicTest.dynamicTest("test" + n,
        () -> assertTrue(n % 2 == 0)));
}

@TestFactoryStreamを返す場合、すべてのテストが実行されると自動的に閉じられることに注意してください。

出力は最初の例とほとんど同じになります。 これには、動的テストに渡す表示名が含まれます。

4. DynamicTestsStreamを作成する

デモンストレーションの目的で、 DomainNameResolver を検討してください。これは、ドメイン名を入力として渡すときにIPアドレスを返します。

簡単にするために、ファクトリメソッドの高レベルのスケルトンを見てみましょう。

@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {

    // sample input and output
    List<String> inputList = Arrays.asList(
      "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com");
    List<String> outputList = Arrays.asList(
      "154.174.10.56", "211.152.104.132", "178.144.120.156");

    // input generator that generates inputs using inputList
    /*...code here...*/

    // a display name generator that creates a 
    // different name based on the input
    /*...code here...*/

    // the test executor, which actually has the 
    // logic to execute the test case
    /*...code here...*/

    // combine everything and return a Stream of DynamicTest
    /*...code here...*/
}

ここでは、 @TestFactory アノテーションを除いて、DynamicTestに関連するコードはあまりありません。

2つのArrayListは、それぞれDomainNameResolverへの入力および期待される出力として使用されます。

次に、入力ジェネレーターを見てみましょう。

Iterator<String> inputGenerator = inputList.iterator();

入力ジェネレータは、StringIteratorに他なりません。 inputList を使用して、ドメイン名を1つずつ返します。

表示名ジェネレーターは非常に単純です。

Function<String, String> displayNameGenerator 
  = (input) -> "Resolving: " + input;

表示名ジェネレーターのタスクは、JUnitレポートまたはIDEの[JUnit]タブで使用されるテストケースの表示名を提供することだけです。

ここでは、ドメイン名を使用して、各テストの一意の名前を生成しています。 一意の名前を作成する必要はありませんが、障害が発生した場合に役立ちます。 これにより、テストケースが失敗したドメイン名を知ることができます。

次に、テストの中心部分であるテスト実行コードを見てみましょう。

DomainNameResolver resolver = new DomainNameResolver();
ThrowingConsumer<String> testExecutor = (input) -> {
    int id = inputList.indexOf(input);
 
    assertEquals(outputList.get(id), resolver.resolveDomain(input));
};

テストケースの作成には、ThrowingConsumerを使用しました。これは@FunctionalInterfaceです。 データジェネレーターによって生成された入力ごとに、 outputList からの期待される出力と、DomainNameResolverのインスタンスからの実際の出力をフェッチしています。

最後の部分は、すべてのピースを組み立てて、DynamicTestStreamとして返すことです。

return DynamicTest.stream(
  inputGenerator, displayNameGenerator, testExecutor);

それでおしまい。 テストを実行すると、表示名ジェネレーターによって定義された名前を含むレポートが表示されます。

Resolving: www.somedomain.com(dynamicTestsFromStream())
Resolving: www.anotherdomain.com(dynamicTestsFromStream())
Resolving: www.yetanotherdomain.com(dynamicTestsFromStream())

5. Java8機能を使用したDynamicTestの改善

前のセクションで記述したテストファクトリは、Java8の機能を使用することで大幅に改善できます。 結果のコードははるかにクリーンになり、より少ない行数で記述できます。

@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamInJava8() {
        
    DomainNameResolver resolver = new DomainNameResolver();
        
    List<String> domainNames = Arrays.asList(
      "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com");
    List<String> outputList = Arrays.asList(
      "154.174.10.56", "211.152.104.132", "178.144.120.156");
        
    return inputList.stream()
      .map(dom -> DynamicTest.dynamicTest("Resolving: " + dom, 
        () -> {int id = inputList.indexOf(dom);
 
      assertEquals(outputList.get(id), resolver.resolveDomain(dom));
    }));       
}

上記のコードは、前のセクションで見たものと同じ効果があります。 inputList.stream()。map()は、入力のストリーム(入力ジェネレーター)を提供します。 dynamicTest()の最初の引数は、表示名ジェネレーター(“ Resolving:” + dom )であり、2番目の引数 lambda は、テストエグゼキューターです。 。

出力は前のセクションのものと同じになります。

6. 追加の例

この例では、テストケースに基づいて入力をフィルタリングする動的テストの能力をさらに調査しています。

@TestFactory
Stream<DynamicTest> dynamicTestsForEmployeeWorkflows() {
    List<Employee> inputList = Arrays.asList(
      new Employee(1, "Fred"), new Employee(2), new Employee(3, "John"));
        
    EmployeeDao dao = new EmployeeDao();
    Stream<DynamicTest> saveEmployeeStream = inputList.stream()
      .map(emp -> DynamicTest.dynamicTest(
        "saveEmployee: " + emp.toString(), 
          () -> {
              Employee returned = dao.save(emp.getId());
              assertEquals(returned.getId(), emp.getId());
          }
    ));
        
    Stream<DynamicTest> saveEmployeeWithFirstNameStream 
      = inputList.stream()
      .filter(emp -> !emp.getFirstName().isEmpty())
      .map(emp -> DynamicTest.dynamicTest(
        "saveEmployeeWithName" + emp.toString(), 
        () -> {
            Employee returned = dao.save(emp.getId(), emp.getFirstName());
            assertEquals(returned.getId(), emp.getId());
            assertEquals(returned.getFirstName(), emp.getFirstName());
        }));
        
    return Stream.concat(saveEmployeeStream, 
      saveEmployeeWithFirstNameStream);
}

save(Long)メソッドには、employeeIdのみが必要です。 したがって、すべてのEmployeeインスタンスを利用します。 save(Long、String)メソッドには、 employeeId とは別に、firstNameが必要です。 したがって、firstName。のないEmployeeインスタンスを除外します。

最後に、両方のストリームを組み合わせて、すべてのテストを単一のStreamとして返します。

それでは、出力を見てみましょう。

saveEmployee: Employee 
  [id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows())
saveEmployee: Employee 
  [id=2, firstName=](dynamicTestsForEmployeeWorkflows())
saveEmployee: Employee 
  [id=3, firstName=John](dynamicTestsForEmployeeWorkflows())
saveEmployeeWithNameEmployee 
  [id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows())
saveEmployeeWithNameEmployee 
  [id=3, firstName=John](dynamicTestsForEmployeeWorkflows())

7. 結論

パラメータ化されたテストは、この記事の例の多くを置き換えることができます。 ただし、動的テストはパラメーター化されたテストとは異なり、完全なテストライフサイクルをサポートしていませんが、パラメーター化されたテストはサポートしています。

さらに、動的テストは、入力の生成方法とテストの実行方法に関してより柔軟性を提供します。

JUnit 5は、機能の原則よりも拡張機能を優先します。結果として、動的テストの主な目的は、サードパーティのフレームワークまたは拡張機能の拡張ポイントを提供することです。

JUnit 5のその他の機能の詳細については、JUnit5での繰り返しテストに関するの記事を参照してください。

GitHubでこの記事の完全なソースコードを確認することを忘れないでください。