Junit 5の動的テストガイド

1概要

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

JUnit 5を初めて使用する場合は、リンク/junit-5-preview[JUnit 5のプレビュー]およびリンク:/junit-5[当社の主なガイド]を確認してください。

2 DynamicTest とは何ですか?

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

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

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

3 DynamicTests を作成する

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

ここに挙げた例は本質的に動的なものではありませんが、本当に動的なものを作成するための良い出発点となるでしょう。

DynamicTest Collection を作成します。

@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に伝えます。ご覧のとおり、 Collection of DynamicTest が返されるだけです。 それぞれの DynamicTest は2つの部分、テストの名前または表示名、および Executable で構成されています。

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

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)));
}

@ TestFactory Stream を返す場合、すべてのテストが実行されると自動的に閉じられます。

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

4 Stream / DynamicTests の作成

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

わかりやすくするために、ファクトリメソッドの上位スケルトンを見てみましょう。

@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...** /}

DesticFactory アノテーションは別として、ここでは DynamicTest に関連したコードはあまりありません。

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

それでは、入力ジェネレータを見てみましょう。

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

入力ジェネレータは String Iterator に他なりません。それは私たちの 入力リスト を使い、ドメイン名を一つずつ返します。

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

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));
};

テストケースを書くための @ FunctionalInterface である ThrowingConsumer を使いました。データジェネレータによって生成された各入力について、 outputList からの期待される出力と DomainNameResolver のインスタンスからの実際の出力を取得します。

最後の部分は、すべてのピースを集めて、 DynamicTest Stream として返すことです。

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

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

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

** 5 Java 8の機能を使用した 動的テスト の改善

前のセクションで書いたテストファクトリは、Java 8の機能を使うことで劇的に改善することができます。結果として得られるコードはずっときれいになり、少ない行数で書くことができます。

@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はhttps://github.com/junit-team/junit5/wiki/Core-Principles[機能の拡張]の原則を優先しています** その結果、動的テストの主な目的はサードパーティに拡張ポイントを提供することです。フレームワークまたは拡張。

JUnit 5の他の機能の詳細については、/junit-5-repeat-test[JUnit 5での繰り返しテストに関する記事]を参照してください。

このhttps://github.com/eugenp/tutorials/tree/master/testing-modules/junit-5[article on GitHub]の完全なソースコードをチェックすることを忘れないでください。