Junit 5の動的テストガイド

Junit 5の動的テストのガイド

1. 概要

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

JUnit 5を初めて使用する場合は、preview of JUnit 5our primary guideを確認することをお勧めします。

2. DynamicTestとは何ですか?

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

@TestFactoryメソッドは、DynamicTestインスタンスのStreamCollectionIterable、またはIteratorを返す必要があります。 コンパイル時に無効な戻り値の型を検出できないため、他のものを返すとJUnitExceptionになります。 これとは別に、@TestFactoryメソッドをstaticまたはprivateにすることはできません。

DynamicTestsは、標準の@Testsとは異なる方法で実行され、ライフサイクルコールバックをサポートしていません。 つまり、@BeforeEach and the @AfterEach methods will not be called for the DynamicTestsです。

3. DynamicTestsの作成

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

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

DynamicTestCollectionを作成します。

@TestFactory
Collection 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のみを返します。 Each of the DynamicTest consists of two parts, the name of the test or the display name, and an Executable

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

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

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

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

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

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

4. DynamicTestsStreamを作成する

デモンストレーションの目的で、ドメイン名を入力として渡すときにIPアドレスを返すDomainNameResolverについて考えてみます。

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

@TestFactory
Stream dynamicTestsFromStream() {

    // sample input and output
    List inputList = Arrays.asList(
      "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com");
    List 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つのArrayListsは、それぞれDomainNameResolverへの入力および期待される出力として使用されます。

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

Iterator inputGenerator = inputList.iterator();

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

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

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

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

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

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

DomainNameResolver resolver = new DomainNameResolver();
ThrowingConsumer testExecutor = (input) -> {
    int id = inputList.indexOf(input);

    assertEquals(outputList.get(id), resolver.resolveDomain(input));
};

テストケースの作成には@FunctionalInterfaceであるThrowingConsumerを使用しました。 データジェネレーターによって生成された入力ごとに、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. Java 8機能を使用したDynamicTestの改善

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

@TestFactory
Stream dynamicTestsFromStreamInJava8() {

    DomainNameResolver resolver = new DomainNameResolver();

    List domainNames = Arrays.asList(
      "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com");
    List 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()の最初の引数は表示名ジェネレーター(「解決中:」+dom)であり、2番目の引数lambdaはテストエグゼキューターです。

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

6. 追加の例

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

@TestFactory
Stream dynamicTestsForEmployeeWorkflows() {
    List inputList = Arrays.asList(
      new Employee(1, "Fred"), new Employee(2), new Employee(3, "John"));

    EmployeeDao dao = new EmployeeDao();
    Stream saveEmployeeStream = inputList.stream()
      .map(emp -> DynamicTest.dynamicTest(
        "saveEmployee: " + emp.toString(),
          () -> {
              Employee returned = dao.save(emp.getId());
              assertEquals(returned.getId(), emp.getId());
          }
    ));

    Stream 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. 結論

パラメーター化されたテストは、この記事の例の多くを置き換えることができます。 However, the dynamic tests differ from the parameterized tests as they support full test lifecycle, while parametrized tests don’t.

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

JUnit 5 prefers extensions over features principle.結果として、動的テストの主な目的は、サードパーティのフレームワークまたは拡張機能の拡張ポイントを提供することです。

JUnit 5の他の機能の詳細については、article on repeated tests in JUnit 5を参照してください。

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