68
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JUnit 5 ユーザガイド 第3章 テストを書く

Last updated at Posted at 2018-04-23

はじめに

株式会社Udzukiの@tsukakeiです。
東京大学大学院でソフトウェアテストの研究を行なっていました。

JUnit 5のユーザガイドを邦訳していきたいと思います。

JUnit 5 ユーザガイド 第1章 概要
JUnit 5 ユーザガイド 第2章 インストール
JUnit 5 ユーザガイド 第3章 テストを書く
JUnit 5 ユーザガイド 第4章 テストを実行する
JUnit 5 ユーザガイド 第5章 モデルの拡張
JUnit 5 ユーザガイド 第6章 JUnit4との違い
JUnit 5 ユーザガイド 第7章 先進的な話題
JUnit 5 ユーザガイド 第8章 APIの進化

全ての章を翻訳し、こちらにアップロードしました。

テストを書く

最初のテストケース

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class FirstJUnit5Tests {

    @Test
    void myFirstTest() {
        assertEquals(2, 1 + 1);
    }

}

アノテーション

JUnit Jupiterは、テストの設定やフレームワークの拡張のために次のアノテーションをサポートしています。

全てのコアなアノテーションは、junit-jupiter-apiモジュール内のorg.junit.jupiter.apiパッケージにあります。

アノテーション 説明
@Test テストメソッドであることを意味します。JUnit 4の@Testと異なり、このアノテーションはどのような属性も宣言しません。これは、JUnit Jupiterにおけるテスト拡張が専用のアノテーションを元に作動するからです。メソッドはオーバーライドされない限り、継承されます。
@ParameterizedTest パラメータ化テストであることを意味します。メソッドはオーバーライドされない限り、継承されます。
@RepeatedTest 繰り返しテストのファクトリーメソッドであることを意味します。メソッドはオーバーライドされない限り、継承されます。
@TestFactory 動的テストのファクトリーメソッドであることを意味します。メソッドはオーバーライドされない限り、継承されます。
@TestInstance テストインスタンス・ライフサイクルを設定するためにクラスに付与します。アノテーションは継承されます。
@TestTemplate テストケースのテンプレートメソッドであることを意味します。テンプレートは登録されたプロバイダによって返される呼び出しコンテキストの数に応じて複数回呼び出されます。
@DisplayName テストクラス、もしくはテストメソッドのカスタム表示名を意味します。アノテーションは継承されません
@BeforeEach 現在のクラス内にある各テスト(@Test, @RepeatedTest, @ParameterizedTest, @TestFactory)が実行される(before)に実行されるメソッドを意味します。JUnit 4の@Beforeと類似したものです。メソッドはオーバーライドされない限り、継承されます。
@AfterEach 現在のクラス内にある各テスト(@Test, @RepeatedTest, @ParameterizedTest, @TestFactory)が実行された(after)に実行されるメソッドを意味します。JUnit 4の@Afterと類似したものです。メソッドはオーバーライドされない限り、継承されます。
@BeforeAll 現在のクラス内にある全テスト(@Test, @RepeatedTest, @ParameterizedTest, @TestFactory)が実行される(before)に実行されるメソッドを意味します。JUnit 4の@BeforeClassと類似したものです。メソッドは隠されるかオーバーライドされない限り、継承され、staticでないといけません("クラスごと"テストインスタンス・ライフサイクルを使わない限り)。
@AfterAll 現在のクラス内にある全テスト(@Test, @RepeatedTest, @ParameterizedTest, @TestFactory)が実行された(after)に実行されるメソッドを意味します。JUnit 4の@AfterClassと類似したものです。メソッドは隠されるかオーバーライドされない限り、継承され、staticでないといけません("クラスごと"テストインスタンス・ライフサイクルを使わない限り)。
@Nested ネストされたnon-staticなテストクラスであることを意味します。@BeforeAll@AfterAllメソッドは、"クラスごと"テストインスタンス・ライフサイクルを使わない限り、@Nestedクラスの中では直接使うことができません。
@Tag テストをクラスレベル、もしくはメソッドレベルでフィルタリングするためのタグを宣言することができます。TestNGのtest groups、もしくはJUnit 4のCategoriesと類似したものです。アノテーションはクラスレベルでは継承されますが、メソッドレベルでは継承されません。
@Disabled テストクラス、もしくはテストメソッドを無効化することができます。JUnit 4の@Ignoreと類似したものです。アノテーションは継承されません
@ExtendWith カスタム拡張を登録することができます。アノテーションは継承されます。

次のアノテーションをつけたメソッドは値を返してはいけません
(@Test, @TestTemplate, @RepeatedTest, @BeforeAll, @AfterAll, @BeforeEach, @AfterEach)

:no_entry_sign: 注意点:いくつかのアノテーションは現在 experimentalにあるかもしれません。詳細に関しては、Experimental APIsをご覧ください。

メタアノテーションとアノテーションの組み合わせ

JUnit Jupiterアノテーションはメタアノテーションとして使うことができます。それはつまり、メタアノテーションの意味を自動で継承する独自の組み合わせアノテーションを定義することができるということです。

例えば、コードベースに@Tag("fast")をコピー&ペーストする代わりに、あなたは@Fastというカスタム組み合わせアノテーションを、次のように作ることができます。@Fast@Tag("fast")の代替として使うことができます。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.Tag;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}

標準的なテストクラス

標準的なテストケース

import static org.junit.jupiter.api.Assertions.fail;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class StandardTests {

    @BeforeAll
    static void initAll() {
    }

    @BeforeEach
    void init() {
    }

    @Test
    void succeedingTest() {
    }

    @Test
    void failingTest() {
        fail("a failing test");
    }

    @Test
    @Disabled("for demonstration purposes")
    void skippedTest() {
        // not executed
    }

    @AfterEach
    void tearDown() {
    }

    @AfterAll
    static void tearDownAll() {
    }

}

情報:テストクラスもテストメソッドも publicである必要はありません

表示名

テストクラスとテストメソッドはカスタムされた表示名(スペース、特殊文字、さらには絵文字だって使えます。)を宣言することができます。それらがテストランナーとテストレポートによって表示されます。

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("A special test case")
class DisplayNameDemo {

    @Test
    @DisplayName("Custom test name containing spaces")
    void testWithDisplayNameContainingSpaces() {
    }

    @Test
    @DisplayName("╯°□°)╯")
    void testWithDisplayNameContainingSpecialCharacters() {
    }

    @Test
    @DisplayName("😱")
    void testWithDisplayNameContainingEmoji() {
    }

}

アサーション

JUnit Jupiterには、JUnit 4が持っていたアサーションメソッドの多くを持っています。また、いくつかはJava 8のラムダ式で使うことができます。全てのJUnit Jupiterアサーションは、org.junit.jupiter.api.Assertionsクラスのstaticメソッドです。

import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

class AssertionsDemo {

    @Test
    void standardAssertions() {
        assertEquals(2, 2);
        assertEquals(4, 4, "The optional assertion message is now the last parameter.");
        assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
                + "to avoid constructing complex messages unnecessarily.");
    }

    @Test
    void groupedAssertions() {
        // In a grouped assertion all assertions are executed, and any
        // failures will be reported together.
        assertAll("person",
            () -> assertEquals("John", person.getFirstName()),
            () -> assertEquals("Doe", person.getLastName())
        );
    }

    @Test
    void dependentAssertions() {
        // Within a code block, if an assertion fails the
        // subsequent code in the same block will be skipped.
        assertAll("properties",
            () -> {
                String firstName = person.getFirstName();
                assertNotNull(firstName);

                // Executed only if the previous assertion is valid.
                assertAll("first name",
                    () -> assertTrue(firstName.startsWith("J")),
                    () -> assertTrue(firstName.endsWith("n"))
                );
            },
            () -> {
                // Grouped assertion, so processed independently
                // of results of first name assertions.
                String lastName = person.getLastName();
                assertNotNull(lastName);

                // Executed only if the previous assertion is valid.
                assertAll("last name",
                    () -> assertTrue(lastName.startsWith("D")),
                    () -> assertTrue(lastName.endsWith("e"))
                );
            }
        );
    }

    @Test
    void exceptionTesting() {
        Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("a message");
        });
        assertEquals("a message", exception.getMessage());
    }

    @Test
    void timeoutNotExceeded() {
        // The following assertion succeeds.
        assertTimeout(ofMinutes(2), () -> {
            // Perform task that takes less than 2 minutes.
        });
    }

    @Test
    void timeoutNotExceededWithResult() {
        // The following assertion succeeds, and returns the supplied object.
        String actualResult = assertTimeout(ofMinutes(2), () -> {
            return "a result";
        });
        assertEquals("a result", actualResult);
    }

    @Test
    void timeoutNotExceededWithMethod() {
        // The following assertion invokes a method reference and returns an object.
        String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
        assertEquals("Hello, World!", actualGreeting);
    }

    @Test
    void timeoutExceeded() {
        // The following assertion fails with an error message similar to:
        // execution exceeded timeout of 10 ms by 91 ms
        assertTimeout(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100);
        });
    }

    @Test
    void timeoutExceededWithPreemptiveTermination() {
        // The following assertion fails with an error message similar to:
        // execution timed out after 10 ms
        assertTimeoutPreemptively(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100);
        });
    }

    private static String greeting() {
        return "Hello, World!";
    }

}

また、JUnit JupiterのいくつかのアサーションメソッドはKotlinで使うことができます。全てのJUnit Jupiter Kotlinアサーションは、org.junit.jupiter.apiパッケージのトップレベル関数です。

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.assertThrows

class AssertionsKotlinDemo {

    @Test
    fun `grouped assertions`() {
        assertAll("person",
            { assertEquals("John", person.firstName) },
            { assertEquals("Doe", person.lastName) }
        )
    }

    @Test
    fun `exception testing`() {
        val exception = assertThrows<IllegalArgumentException> ("Should throw an exception") {
            throw IllegalArgumentException("a message")
        }
        assertEquals("a message", exception.message)
    }

    @Test
    fun `assertions from a stream`() {
        assertAll(
            "people with name starting with J",
            people
                .stream()
                .map {
                    // This mapping returns Stream<() -> Unit>
                    { assertTrue(it.firstName.startsWith("J")) }
                }
        )
    }

    @Test
    fun `assertions from a collection`() {
        assertAll(
            "people with last name of Doe",
            people.map { { assertEquals("Doe", it.lastName) } }
        )
    }

}

サードパーティによるアサーションライブラリ

JUnit Jupiterによって提供されているアサーション機能は、多くのテストシナリオには十分なものだと思いますが、matchersのような、より強力で追加的な機能が求められたり、必要であることがあります。そのような場合、JUnitチームは、AssertJHamcrestTruthなどといったサードパーティによるアサーションライブラリの使用をお薦めします。したがって、開発者は自由に選んだアサーションライブラリを使うことができます。

例えば、matchersと流暢なAPI (fluent API) の組み合わせは、アサーションをよりわかりやすく、読みやすくするために使うことができます。しかしながら、JUnit Jupiterのorg.junit.jupiter.Assertionsクラスは、HamcrestのMatcherを受け入れていたJUnit 4のorg.junit.jupiter.AssertクラスのassertThat()メソッドのようなものを提供していません。代わりに、開発者はサードパーティによるアサーションライブラリによって提供されているマッチャー用のサポートを使うことが奨励されています。

次の例は、JUnit Jupiterテストにおいて、どのようにしてHamcrestからのassertThat()サポートを使うかを説明しています。Hamcrestライブラリがクラスパスに加えられている限り、assertThat()is()equalTo()といったメソッドをstaticにインポートすることができます。また、それらをテストの中で、下に示すassertWithHamcrestMatcher()のように使うことができます。

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import org.junit.jupiter.api.Test;

class HamcrestAssertionDemo {

    @Test
    void assertWithHamcrestMatcher() {
        assertThat(2 + 1, is(equalTo(3)));
    }

}

当然、JUnit 4のプログラミングモデルに基づいたレガシーコードもorg.junit.Assert#assertThatを使い続けることができます。

アサンプション

JUnit Jupiterには、JUnit 4が持っていたアサンプションメソッドの多くを持っています。また、いくつかはJava 8のラムダ式で使うことができます。全てのJUnit Jupiterアサンプションは、org.junit.jupiter.api.Assumptionsクラスのstaticメソッドです。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;

import org.junit.jupiter.api.Test;

class AssumptionsDemo {

    @Test
    void testOnlyOnCiServer() {
        assumeTrue("CI".equals(System.getenv("ENV")));
        // remainder of test
    }

    @Test
    void testOnlyOnDeveloperWorkstation() {
        assumeTrue("DEV".equals(System.getenv("ENV")),
            () -> "Aborting test: not on developer workstation");
        // remainder of test
    }

    @Test
    void testInAllEnvironments() {
        assumingThat("CI".equals(System.getenv("ENV")),
            () -> {
                // perform these assertions only on the CI server
                assertEquals(2, 2);
            });

        // perform these assertions in all environments
        assertEquals("a string", "a string");
    }

}

テストの無効化

テストクラス全体、もしくは各テストメソッドは@Disabledアノテーションか、条件付きテスト実行で話されているアノテーションの一つ、もしくはカスタム実行条件によって無効化することができます。

これは@Disabledテストクラスです。

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@Disabled
class DisabledClassDemo {
    @Test
    void testWillBeSkipped() {
    }
}

そして、これは@Disabledテストメソッドを含むテストクラスです。

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class DisabledTestsDemo {

    @Disabled
    @Test
    void testWillBeSkipped() {
    }

    @Test
    void testWillBeExecuted() {
    }
}

条件付きテスト実行

JUnit Jupiterの実行条件拡張APIによって、開発者は、コンテナ、またはある条件に基づいたテストをプログラム的に可能にしたり不可能することができます。そのような条件の最も単純な例は、@DisabledをサポートしているビルトインのDisabledConditionです(テストを不可能にするをご覧ください)。@Disabledに加えて、JUnit Jupiterは、org.junit.jupiter.api.conditionパッケージ内の他のいくつかのアノテーションベースの条件もサポートしています。org.junit.jupiter.api.conditionパッケージによって、開発者はコンテナやテストを宣言的に可能にしたり不可能にすることができます。詳細については、次のセクションをご覧ください。

:bulb: 組み合わせアノテーションのヒント:次のセクションで挙げられている条件付きアノテーションはどれも、組み合わせアノテーションを作るためのメタアノテーションとして使えるかもしれない。例えば、@EnabledOnOsのデモにある@TestOnMacアノテーションは、どのようにして@Test@EnableOnOsを一つの再利用できるアノテーションで結合できるかを示しています。

:no_entry_sign: 次のセクションで挙げられている条件付きアノテーションはそれぞれ、テストインターフェイス、テストクラス、またはテストメソッドに一度だけ宣言することができます。もし条件付きアノテーションが直接的か間接的、またはメタ的に、ある要素に複数存在していた場合、JUnitによって発見された初めのアノテーションが使われます;他の追加的なアノテーションは無視されます。しかしながら、org.junit.jupiter.api.conditionパッケージでは、各条件付きアノテーションは他の条件付きアノテーションと共に使われているかもしれません。

オペレーティングシステムの条件

@EnabledOnOs@DisabledOnOsを使うことで、特定のオペーティングシステム上でコンテナかテストを、有効にしたり無効にすることができます。

@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
    // ...
}

@TestOnMac
void testOnMac() {
    // ...
}

@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
    // ...
}

@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
    // ...
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@EnabledOnOs(MAC)
@interface TestOnMac {
}

Java実行時条件

@EnabledOnJre@DisabledOnJreを使うことで、特定のバージョンのJava実行環境(JRE)でコンテナかテストを、有効にしたり無効にすることができます。

@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
    // ...
}

@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
    // ...
}

@Test
@DisabledOnJre(JAVA_9)
void notOnJava9() {
    // ...
}

システムプロパティ条件

@EnabledIfSystemProperty@DisabledIfSystemPropertyを使うことで、namedで指定したJVMシステムプロパティの値に応じてコンテナかテストを、有効にしたり無効にすることができます。matches属性を使うことで、値は正規表現として解釈されます。

@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
    // ...
}

@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {
    // ...
}

環境変数条件

@EnabledIfEnvironmentVariable@DisabledIfEnvironmentVariableを使うことで、namedで指定したOSの環境変数の値に応じてコンテナかテストを、有効にしたり無効にすることができます。matches属性を使うことで、値は正規表現として解釈されます。

@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {
    // ...
}

@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
void notOnDeveloperWorkstation() {
    // ...
}

スクリプトベースの条件

JUnit Jupiterは、@EnabledIf@DisabledIfを使うことで、設定されたスクリプトの評価値に応じてコンテナかテストを、有効にしたり無効にすることができる機能を提供しています。スクリプトは、JavaScriptかGroovy、またはJSR 223で定義されたJava Scripting APIをサポートしているスクリプト言語であれば何でも、書くことができます。

:no_entry_sign: @EnabledIf@DisabledIfを使った条件付きテスト実行は、現在実験的な機能です。詳細については、Experimental APIsをご覧ください。

:bulb: もしスクリプトのロジックが、今使っているOSか、JREのバージョン、またはあるJVMシステムプロパティや環境変数に依存している場合、その目的に合ったビルトインアノテーションを使うといいかもしれません。詳細については、この章の前のセクションをご覧ください。

:information_source: もし同じスクリプトベースの条件を多数使っている場合、より速く、型安全で、メンテナンスのしやすい条件を実装するために、それに合った実行条件拡張を書くことを考えてみてください。

@Test // Static JavaScript expression.
@EnabledIf("2 * 3 == 6")
void willBeExecuted() {
    // ...
}

@RepeatedTest(10) // Dynamic JavaScript expression.
@DisabledIf("Math.random() < 0.314159")
void mightNotBeExecuted() {
    // ...
}

@Test // Regular expression testing bound system property.
@DisabledIf("/32/.test(systemProperty.get('os.arch'))")
void disabledOn32BitArchitectures() {
    assertFalse(System.getProperty("os.arch").contains("32"));
}

@Test
@EnabledIf("'CI' == systemEnvironment.get('ENV')")
void onlyOnCiServer() {
    assertTrue("CI".equals(System.getenv("ENV")));
}

@Test // Multi-line script, custom engine name and custom reason.
@EnabledIf(value = {
                "load('nashorn:mozilla_compat.js')",
                "importPackage(java.time)",
                "",
                "var today = LocalDate.now()",
                "var tomorrow = today.plusDays(1)",
                "tomorrow.isAfter(today)"
            },
            engine = "nashorn",
            reason = "Self-fulfilling: {result}")
void theDayAfterTomorrow() {
    LocalDate today = LocalDate.now();
    LocalDate tomorrow = today.plusDays(1);
    assertTrue(tomorrow.isAfter(today));
}

スクリプトバインディング

次の名前は、各スクリプト条件とバインドされています。そのため、スクリプトで使用可能です。accessorは、単純な String get(String name)メソッドを通して、マップライクな構造へのアクセスを提供します。

Name Type Description
systemEnvironment accessor OS環境変数アクセサ
systemProperty accessor JVMシステムプロパティアクセサ
junitConfigurationParameter accessor Configurationパラメータアクセサ
junitDisplayName String テストかコンテナの表示名
junitTags Set<String> テストかコンテナに振られている全てのタグ
junitUniqueId String テストかコンテナのユニークなID

タグとフィルタリング

テストクラスとメソッドは@Tagを用いてタグ付けすることができます。それらのタグは、後でテスト発見と実行をフィルタリングするのに使われます。

タグのシンタックスルール

  • タグはnullあってはならない
  • トリミングされたタグは空白文字を含んではならない。
  • トリミングされたタグはISO制御文字を含んではならない。
  • トリミングされたタグは次の予約語のいずれも含んではならない。
    • ,:カンマ
    • (:左カッコ
    • ):右カッコ
    • &:アンパサンド
    • |:縦棒
    • !:エクスクラメーション

:information_source: 上の文章で、トリミングされたというのは、語頭と語尾の空白文字を取り除いたということを意味します。

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag("fast")
@Tag("model")
class TaggingDemo {

    @Test
    @Tag("taxes")
    void testingTaxCalculation() {
    }

}

テストインスタンス・ライフサイクル

各テストメソッドの独立した実行と、変化可能なテストインスタンスの状態による予期せぬ副作用を避けるため、JUnitは各テストメソッドを実行する前に、各テストクラスの新しいインスタンスを生成します(何がテストメソッドなのかは、下の注意書きをご覧ください)。この"メソッドごと"のテストインスタンス・ライフサイクルはJUnit Jupiterではデフォルトの動作で、以前の全てのバージョンのJUnitと類似したものになっています。

:information_source: @Disabled@DisabledOnOsといった条件によって無効化されたテストメソッドであっても、テストクラスはインスタンス化されることに注意してください。これは例えば、"クラスごと"テストインスタンス・ライフサイクルが有効である時でも同様です。

もしJUnit Jupiterに全テストメソッドを同じテストインスタンスで実行してほしい場合は、単にテストクラスに@TestInstance(Lifecycle.PER_CLASS)アノテーションを付与するだけで実現可能です。このモードを使用する場合、新しいテストインスタンスは一度だけ生成されます。これによって、もしテストメソッドがインスタンス変数に保存された状態に依存する場合は、@BeforeEachAfterEachメソッドによって状態をリセットする必要があるかもしれません。

”クラスごと”モードは、デフォルトの"メソッドごと"モードに比べていくつかの付加的な便益があります。特に"クラスごと"モードを使うと、インターフェイスのdefaultメソッドと同様に、@BeforeAll@AfterAllメソッドをnon-staticメソッドとして宣言することが可能になります。そのため、"クラスごと"モードでは、@Nestedテストクラス内で@BeforeAll@AfterAllメソッドを使うことができます。

Kotlinでテストを書いている場合、”クラスごと”テストインスタンス・ライフサイクルモードに切り替えることで、@BeforeAll@AfterAllメソッドの実装がより容易になるかもしれません。

:information_source: テストインスタンス・ライフサイクルの文脈内におけるテストメソッドは、@Test@RerpeatedTest@ParameterizedTest@TestFactory@TestTemplateが付与されたメソッド全てのことを意味します。

デフォルトのテストインスタンス・ライフサイクルを変更する

テストクラスかテストインターフェイスに@TestInstanceが付与されていない場合、JUnit Jupiterはデフォルトのライフサイクルモードを使います。
標準的なデフォルトモードはPER_METHODです;しかしながら、テスト計画全体のデフォルトを変更することが可能です。デフォルトのテストインスタンス・ライフサイクルを変更するには、単にjunit.jupiter.testinstance.lifecycle.default設定パラメータに、TestInstance.Lifecycleで定義されたenum定数の値を、大文字・小文字を無視してセットするだけです。これは、JVMシステムプロパティとして渡すか、Launcherに渡されるLauncherDiscoveryRequest内の設定パラメータとして渡すか、JUnitプラットフォーム設定ファイル(詳細については、Configuration Parametersをご覧ください。)を通して渡します。

例えば、デフォルトのテストインスタンス・ライフサイクルをLifeCycle.PER_CLASSに設定するには、JVMを次のシステムプロパティで起動することです。
-Djunit.jupiter.testinstance.lifecycle.default=per_class

しかしながら、JUnitプラットフォーム設定ファイルを通してデフォルトのテストインスタンス・ライフサイクルを設定する場合は、次の内容を含んだjunit-platform.propertiesという名前のファイルをクラスパスのルート(例えば、src/test/resources)に生成する必要があることに注意してください。

junit.jupiter.testinstance.lifecycle.default = per_class

:no_entry_sign: デフォルトのテストインスタンス・ライフサイクルを変更することは、一貫性を持って適用しないと、予測不可能な結果と壊れやすいビルドを導く恐れがあります。例えば、デフォルトとして”クラスごと”をビルド設定していながら、IDE上で"メソッドごと"でテスト実行されてしまった場合、ビルドサーバに表れるエラーをデバッグすることは困難になる恐れがあります。そのため、JVMシステムプロパティの代わりに、JUnitプラットフォーム設定ファイルを使ってデフォルトモードを変更することをお薦めします。

ネストされたテスト

ネストされたテストは、テスト開発者が様々なグループのテスト間の関係を表現することを可能にします。これがその例です。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.EmptyStackException;
import java.util.Stack;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, () -> stack.pop());
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, () -> stack.peek());
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

:information_source: non-staticなネストされたクラス(つまり、インナークラス)のみ@Nestedテストクラスとなります。ネストは任意に深くすることができ、それらのインナークラスは一つの例外を除いて、テストクラスの完全なメンバーとして考えられます。例外とは、@BeforeAll@AfterAllがデフォルトでは作動しないことです。その理由は、Javaがインナークラスにstaticなメンバーを許さないためです。しかしながら、この制限は@Nestedテストクラスに@TestInstance(Lifecycle.PER_CLASS)を付与することで回避することができます(テストインスタンス・ライフサイクルをご覧ください)。

コンストラクタとメソッドへの依存性注入

全てのJUnitの全バージョンでは、テストコンストラクタかメソッドは、パラメータを持つことが許されていませんでした(少なくとも標準的なRunner実装の下では)。JUnit Jupiterでの大きな変化の一つとして、テストコンストラクタとテストメソッドのどちらもパラメータを持つことが許されたことがあります。このことは、大きな柔軟性をもたらし、コンストラクタとメソッドに依存性を注入することが可能になりました。

ParameterResolverは、実行時に動的にパラメータを解決することを望むテスト拡張のためのAPIを定義しています。もしテストコンストラクタか、@Test@TestFactory@BeforeEach@AfterEach@BeforeAll@AfterAllメソッドがパラメータを許容する時は、パラメータは登録されたParameterResolverによって実行時に解決されないといけません。

現在は、3つのビルトイン・リゾルバが自動的に登録されます。

  • TestInfoParameterResolver:もしメソッドパラメータがTestInfo型であった場合、TestInfoParameterResolverは、現在のテストに応じたTestInfoのインスタンスをパラメータの値として供給します。TestInfoは、テストの表示名、テストクラス、テストメソッド、付いているタグ名といった現在のテストに関する情報を集めるのに使うことができます。表示名は、テストクラス名かテストメソッド名といった技術的な名前か、@DisplayedNameで設定されたカスタム名のどちらかです。TestInfoは、JUnit 4のTestNameの代替のようなものです。次のコードは、テストコンストラクタ、@BeforeEach@TestメソッドにどのようにTestInfoを注入させるかを示しています。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;

@DisplayName("TestInfo Demo")
class TestInfoDemo {

    TestInfoDemo(TestInfo testInfo) {
        assertEquals("TestInfo Demo", testInfo.getDisplayName());
    }

    @BeforeEach
    void init(TestInfo testInfo) {
        String displayName = testInfo.getDisplayName();
        assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
    }

    @Test
    @DisplayName("TEST 1")
    @Tag("my-tag")
    void test1(TestInfo testInfo) {
        assertEquals("TEST 1", testInfo.getDisplayName());
        assertTrue(testInfo.getTags().contains("my-tag"));
    }

    @Test
    void test2() {
    }

}
  • RepetitionInfoParameterResolver:もし@RepeatedTest@BeforeEach@AfterEachメソッドのパラメータがRepetitionInfo型であった場合、RepetitionInfoParameterResolverは、RepetitionInfoのインスタンスを供給します。RepetitionInfoは、現在の繰り返しと対応する@RepeatedTestの繰り返しの総回数に関する情報を集めるのに使うことができます。しかしながら、RepetitionInfoParameterResolverは、@RepeatedTest以外の文脈以外では登録されていないことに注意してください。繰り返しテストの例をご覧ください。
  • TestReporterParameterResolver:もしメソッドパラメータがTestReporter型であった場合、TestReporterParameterResolverTestReporterのインスタンスを供給します。TestReporterは、現在のテスト実行に関する付加的な情報を公開することに使うことができます。データは、TestExecutionListener.reportingEntryPublished()を通して消費され、それによってIDEから閲覧できたりレポートに含まれます。JUnit Jupiterでは、JUnit 4で stdoutstderrを使って情報を出力していた箇所にTestReporterを使うことができます。@RunWith(JUnitPlatform.class)を使うと、全てのレポートされたエントリをstdoutに出力します。
import java.util.HashMap;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestReporter;

class TestReporterDemo {

    @Test
    void reportSingleValue(TestReporter testReporter) {
        testReporter.publishEntry("a key", "a value");
    }

    @Test
    void reportSeveralValues(TestReporter testReporter) {
        HashMap<String, String> values = new HashMap<>();
        values.put("user name", "dk38");
        values.put("award year", "1974");

        testReporter.publishEntry(values);
    }

}

:information_source: 他のパラメータリゾルバは、@ExtendedWithを用いた適切な拡張を登録することによって明示的に有効化する必要があります。

カスタムParameterResolverの例のために、MockitoExtensionを確認しましょう。リリース可能なものではありませんが、拡張モデルとパラメータ解決プロセス両方の単純さと表現性を例示しています。MyMockitoTestは、どのように@BeforeEach@Testメソッド内にMockitoモックを注入するかを示しています。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import com.example.Person;
import com.example.mockito.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class MyMockitoTest {

    @BeforeEach
    void init(@Mock Person person) {
        when(person.getName()).thenReturn("Dilbert");
    }

    @Test
    void simpleTestWithInjectedMock(@Mock Person person) {
        assertEquals("Dilbert", person.getName());
    }

}

テストインターフェイスとデフォルトメソッド

JUnit Jupiterは、@Test@RepeatedTest@ParameterizedTest@TestFactory@TestTemplate@BeforeEach@AfterEachにインターフェイスデフォルトメソッドを宣言できるようにしています。@BeforeAll@AfrterAllはテストインターフェイス内でstaticメソッドを宣言するか、もしテストクラスに@TestInstance(Lifecycle.PER_CLASS)が付与されている場合はインターフェイスデフォルトメソッドを宣言することができます(テストインスタンス・ライフサイクルをご覧ください)。いくつかの例を示します。

@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {

    static final Logger LOG = Logger.getLogger(TestLifecycleLogger.class.getName());

    @BeforeAll
    default void beforeAllTests() {
        LOG.info("Before all tests");
    }

    @AfterAll
    default void afterAllTests() {
        LOG.info("After all tests");
    }

    @BeforeEach
    default void beforeEachTest(TestInfo testInfo) {
        LOG.info(() -> String.format("About to execute [%s]",
            testInfo.getDisplayName()));
    }

    @AfterEach
    default void afterEachTest(TestInfo testInfo) {
        LOG.info(() -> String.format("Finished executing [%s]",
            testInfo.getDisplayName()));
    }

}
interface TestInterfaceDynamicTestsDemo {

    @TestFactory
    default Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(
            dynamicTest("1st dynamic test in test interface", () -> assertTrue(true)),
            dynamicTest("2nd dynamic test in test interface", () -> assertEquals(4, 2 * 2))
        );
    }

}

@ExtenedWoth@Tagはテストインターフェイスとして宣言することができるため、インターフェイスを実装したクラスは自動的にタグと拡張を継承します。TimingExtensionのソースコードを見るには、BeforeとAfterのテスト実行コールバックをご覧ください。

@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}

テストクラスでは、これらのテストインターフェイスを実装することで適用することができます。

class TestInterfaceDemo implements TestLifecycleLogger,
        TimeExecutionLogger, TestInterfaceDynamicTestsDemo {

    @Test
    void isEqualValue() {
        assertEquals(1, 1, "is always equal");
    }

}

TestInterfaceDemoの実行結果の出力は、次のものと似たようなものになります。

:junitPlatformTest
INFO  example.TestLifecycleLogger - Before all tests
INFO  example.TestLifecycleLogger - About to execute [dynamicTestsFromCollection()]
INFO  example.TimingExtension - Method [dynamicTestsFromCollection] took 13 ms.
INFO  example.TestLifecycleLogger - Finished executing [dynamicTestsFromCollection()]
INFO  example.TestLifecycleLogger - About to execute [isEqualValue()]
INFO  example.TimingExtension - Method [isEqualValue] took 1 ms.
INFO  example.TestLifecycleLogger - Finished executing [isEqualValue()]
INFO  example.TestLifecycleLogger - After all tests

Test run finished after 190 ms
[         3 containers found      ]
[         0 containers skipped    ]
[         3 containers started    ]
[         0 containers aborted    ]
[         3 containers successful ]
[         0 containers failed     ]
[         3 tests found           ]
[         0 tests skipped         ]
[         3 tests started         ]
[         0 tests aborted         ]
[         3 tests successful      ]
[         0 tests failed          ]

BUILD SUCCESSFUL

この機能の他のあり得る適用としては、インターフェイス契約のためにテストを書くことです。例えば、Object.equalsComparable.compareToの実装がどう振る舞うべきかのテストを、次のように書くことができます。

public interface Testable<T> {

    T createValue();

}
public interface EqualsContract<T> extends Testable<T> {

    T createNotEqualValue();

    @Test
    default void valueEqualsItself() {
        T value = createValue();
        assertEquals(value, value);
    }

    @Test
    default void valueDoesNotEqualNull() {
        T value = createValue();
        assertFalse(value.equals(null));
    }

    @Test
    default void valueDoesNotEqualDifferentValue() {
        T value = createValue();
        T differentValue = createNotEqualValue();
        assertNotEquals(value, differentValue);
        assertNotEquals(differentValue, value);
    }

}
public interface ComparableContract<T extends Comparable<T>> extends Testable<T> {

    T createSmallerValue();

    @Test
    default void returnsZeroWhenComparedToItself() {
        T value = createValue();
        assertEquals(0, value.compareTo(value));
    }

    @Test
    default void returnsPositiveNumberComparedToSmallerValue() {
        T value = createValue();
        T smallerValue = createSmallerValue();
        assertTrue(value.compareTo(smallerValue) > 0);
    }

    @Test
    default void returnsNegativeNumberComparedToSmallerValue() {
        T value = createValue();
        T smallerValue = createSmallerValue();
        assertTrue(smallerValue.compareTo(value) < 0);
    }

}

テストクラスは、両方のインターフェイスを実装することができ、それによって対応するテストを継承します。もちろん、抽象メソッドを実装する必要があります。

class StringTests implements ComparableContract<String>, EqualsContract<String> {

    @Override
    public String createValue() {
        return "foo";
    }

    @Override
    public String createSmallerValue() {
        return "bar"; // 'b' < 'f' in "foo"
    }

    @Override
    public String createNotEqualValue() {
        return "baz";
    }

}

:information_source: 上記のテストは、単に例であって、完全ではありません。

繰り返しテスト

JUnit Jupiterは、@RepeatedTestを付与し、繰り返してほしい回数を設定するだけで、特定回数テストを繰り返す機能を提供しています。繰り返しテストの各呼び出しは、通常の@Testメソッドの実行のように振る舞い、全く同じライフサイクル・コールバックと拡張をサポートしています。

次の例は、どのようにして自動で10回繰り返すrepeatedTes()の宣言するかを示しています。

@RepeatedTest(10)
void repeatedTest() {
    // ...
}

繰り返し回数の設定に加えて、@RepeatedTestname属性を用いることで、カスタム表示名も設定することができます。さらに、表示名は、静的テキストと動的プレースホルダの組み合わせのパターンにすることもできます。次のプレースホルダが現在サポートされています。

  • {displayName}:@RepeatedTestメソッドの表示名
  • {currentRepetition}: 現在の繰り返し回数
  • {totalRepetition}: 繰り返し回数の合計

ある繰り返し回数時点でのデフォルトの表示名は、次のパターンに基づいて生成されます:"repetition {currentRepetition} of {totalRepetitions}"。そのため、先ほどの例の各繰り返し回数における表示名は次のようになります:repetition 1 of 10repetition 2 of 10、などなど。もし@RepeatedTestの表示名を、各繰り返し回数での表示名に含めたい場合は、独自のカスタムパターンを定義するか、事前定義されたRepeatedTest.LONG_DISPLAY_NAMEパターンを使うことができます。後者は、"{displayName} :: repetition {currentRepetition} of {totalRepetitions}"と等しいもので、結果はrepeatedTest() :: repetition 1 of 10repeatedTest() :: repetition 2 of 10となります。

現在の繰り返し回数と繰り返しの合計数の情報をプログラムから集めるためには、開発者は@RepeatedTest@BeforeEach、もしくは@AfterEachRepetitionInfoインスタンスを注入することができます。

繰り返しテストの例

このセクションの最後にあるRepeatedTestsDemoクラスは、繰り返しテストのいくつかの例を示しています。

repeatedTest()メソッドは、前のセクションからの例です。一方、repeatedTestWithRepetitionInfo()は、繰り返しの合計数と現在の繰り返し回数を得るために、どのようにしてRepetitionInfoインスタンスをテストに注入すればいいかを示しています。

その次の2つのメソッドは、どのようにして@RepeatedTestのカスタム@DisplayNameを各繰り返しの表示名内に含ませるかを示しています。customDisplayName()は、カスタム表示名とカスタムパターンを組み合わせており、TestInfoを使って生成された表示名のフォーマットを検証しています。Repeat!@DisplayNameから来る{displayName}で、1/1{currentRepetition}/{totalRepetitions}から来ています。対照的に、customDisplayNameWithLongPattern()は、先ほど説明した事前定義のRepeatedTest.LONG_DISPLAY_NAMEパターンを使っています。

repeatedTestInGerman()は、繰り返しテストの表示名を他国言語(この場合はドイツ語です)に翻訳する機能を示しています。その結果、各繰り返しにおける名前は、Wiederholung 1 von 5Wiederholung 2 von 5のようになります。

beforeEach()メソッドは@BeforeEachが付与されているため、各繰り返しテストの各繰り返し前に実行されます。TestInfoRepetitionInfoをこのメソッドに注入することで、現在実行されている繰り返しテストに関する情報を得ることができます。INFOログレベルで、RepeatedTestsDemoを実行すると出力は次のようになります。

INFO: About to execute repetition 1 of 10 for repeatedTest
INFO: About to execute repetition 2 of 10 for repeatedTest
INFO: About to execute repetition 3 of 10 for repeatedTest
INFO: About to execute repetition 4 of 10 for repeatedTest
INFO: About to execute repetition 5 of 10 for repeatedTest
INFO: About to execute repetition 6 of 10 for repeatedTest
INFO: About to execute repetition 7 of 10 for repeatedTest
INFO: About to execute repetition 8 of 10 for repeatedTest
INFO: About to execute repetition 9 of 10 for repeatedTest
INFO: About to execute repetition 10 of 10 for repeatedTest
INFO: About to execute repetition 1 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 2 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 3 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 4 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 5 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 1 of 1 for customDisplayName
INFO: About to execute repetition 1 of 1 for customDisplayNameWithLongPattern
INFO: About to execute repetition 1 of 5 for repeatedTestInGerman
INFO: About to execute repetition 2 of 5 for repeatedTestInGerman
INFO: About to execute repetition 3 of 5 for repeatedTestInGerman
INFO: About to execute repetition 4 of 5 for repeatedTestInGerman
INFO: About to execute repetition 5 of 5 for repeatedTestInGerman
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.logging.Logger;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;

class RepeatedTestsDemo {

    private Logger logger = // ...

    @BeforeEach
    void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
        int currentRepetition = repetitionInfo.getCurrentRepetition();
        int totalRepetitions = repetitionInfo.getTotalRepetitions();
        String methodName = testInfo.getTestMethod().get().getName();
        logger.info(String.format("About to execute repetition %d of %d for %s", //
            currentRepetition, totalRepetitions, methodName));
    }

    @RepeatedTest(10)
    void repeatedTest() {
        // ...
    }

    @RepeatedTest(5)
    void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
        assertEquals(5, repetitionInfo.getTotalRepetitions());
    }

    @RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
    @DisplayName("Repeat!")
    void customDisplayName(TestInfo testInfo) {
        assertEquals(testInfo.getDisplayName(), "Repeat! 1/1");
    }

    @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
    @DisplayName("Details...")
    void customDisplayNameWithLongPattern(TestInfo testInfo) {
        assertEquals(testInfo.getDisplayName(), "Details... :: repetition 1 of 1");
    }

    @RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
    void repeatedTestInGerman() {
        // ...
    }

}

ConsoleLauncherか、unicodeテーマを有効化したjunitPlatformTestGradleプラグインを使うと、RepeatedTestsDemoの実行結果は次のようなコンソール出力を行います。

├─ RepeatedTestsDemo ✔
│  ├─ repeatedTest() ✔
│  │  ├─ repetition 1 of 10 ✔
│  │  ├─ repetition 2 of 10 ✔
│  │  ├─ repetition 3 of 10 ✔
│  │  ├─ repetition 4 of 10 ✔
│  │  ├─ repetition 5 of 10 ✔
│  │  ├─ repetition 6 of 10 ✔
│  │  ├─ repetition 7 of 10 ✔
│  │  ├─ repetition 8 of 10 ✔
│  │  ├─ repetition 9 of 10 ✔
│  │  └─ repetition 10 of 10 ✔
│  ├─ repeatedTestWithRepetitionInfo(RepetitionInfo) ✔
│  │  ├─ repetition 1 of 5 ✔
│  │  ├─ repetition 2 of 5 ✔
│  │  ├─ repetition 3 of 5 ✔
│  │  ├─ repetition 4 of 5 ✔
│  │  └─ repetition 5 of 5 ✔
│  ├─ Repeat! ✔
│  │  └─ Repeat! 1/1 ✔
│  ├─ Details... ✔
│  │  └─ Details... :: repetition 1 of 1 ✔
│  └─ repeatedTestInGerman() ✔
│     ├─ Wiederholung 1 von 5 ✔
│     ├─ Wiederholung 2 von 5 ✔
│     ├─ Wiederholung 3 von 5 ✔
│     ├─ Wiederholung 4 von 5 ✔
│     └─ Wiederholung 5 von 5 ✔

パラメータ化テスト

パラメータ化テストを使うと、テストを異なる引数で複数回実行できるようになります。パラメータ化テストは、通常の@Testの代わりに@ParameterizedTestを付与するだけで宣言することができます。さらに、各呼び出して供給され、テストで消費される引数として、最低でも1つのsourceを宣言する必要があります。

次の例は、パラメータ化テストを示していて、@ValueSourceを使ってString配列を引数のソースに設定しています。

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
    assertTrue(isPalindrome(candidate));
}

上記のパラメータ化テストメソッドを実行すると、各呼び出しは別々に報告されます。例えば、ConsoleLauncherは次のようなものを出力します。

palindromes(String) ✔
├─ [1] racecar ✔
├─ [2] radar ✔
└─ [3] able was I ere I saw elba ✔

:no_entry_sign: パラメータ化テストは、現在実験的な機能です。詳細については、Experimental APIsをご覧ください。

必要なセットアップ

パラメータ化テストを使うためには、junit-jupiter-paramsを依存関係に加える必要があります。詳細については、依存性のメタデータをご覧ください。

引数の消費

典型的なパラメータ化テストメソッドは、引数ソースインデックスとメソッドパラメータインデックス間の1対1相関(@CsvSourceの例をご覧ください)に従って、設定されたソース(引数のソースをご覧ください。)から直接、引数を消費します。しかしながら、パラメータ化テストメソッドは、ソースから得た引数をひとつのオブジェクトに集約して、メソッドに渡すこともできます(引数集約をご覧ください)。付加的な引数もまた、ParameterResolverによって提供されます(例えば、TestInfoTestReporterのインスタンスなど)。特に、パラメータ化テストメソッドは、次のルールに従って形式的なパラメータを宣言する必要があります。

  • 0個以上のインデックスされた引数が最初に宣言されないといけない。
  • 0個以上の集約器が次に宣言されないといけない。
  • 0個以上のParameterResolverによって供給される引数が最後に宣言されないといけない。

この文脈で、インデックスされた引数というのは、ArgumentsProviderによって提供されるArguments内で与えられたインデックスに対応する引数です。インデックスされた引数は、引数として、メソッドの形式パラメータリストと同じインデックスの箇所に渡されます。集約器は、ArgumentsAccessor型の全てのパラメータか、@AggregateWithの付与された全てのパラメータです。

引数のソース

すぐに使えるように、JUnit Jupiterはかなりの数のソースアノテーションを提供しています。次の各セクションはそれぞれ、簡潔な概要とそれぞれの例を提供しています。追加的な情報に関しては、org.junit.jupiter.params.providerパッケージのJavaDocを参照してください。

@ValueSouce

@ValueSourceは最も単純なソースの一つです。リテラル値の配列を1つ設定することができ、パラメータ化テスト呼び出しにつき、1つのパラメータを提供することができます。

次のリテラル値の型が@ValueSourceにサポートされています。

  • short
  • byte
  • int
  • long
  • float
  • double
  • char
  • java.lang.String
  • java.lang.Class

例えば、次の@ParameterizedTestメソッドはそれぞれ123の値とともに3回呼び出されます。

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
    assertTrue(argument > 0 && argument < 4);
}

@EnumSource

@EnumSourceは、Enum定数に対して便利な機能を提供します。@EnumSourceは、使われる定数を特定するために、任意のnamesパラメータを提供します。もし指定されていない場合は、次の例のように全ての定数が使われます。

@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithEnumSource(TimeUnit timeUnit) {
    assertNotNull(timeUnit);
}

@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(TimeUnit timeUnit) {
    assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}

@EnumSourceはまた、テストメソッドに渡すパラメータを細かく制御するために、任意のmodeパラメータを提供します。例えば、次の例では、enum定数プールからnamesを取り除いたり、正規表現を設定しています。

@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { "DAYS", "HOURS" })
void testWithEnumSourceExclude(TimeUnit timeUnit) {
    assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
    assertTrue(timeUnit.name().length() > 5);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$")
void testWithEnumSourceRegex(TimeUnit timeUnit) {
    String name = timeUnit.name();
    assertTrue(name.startsWith("M") || name.startsWith("N"));
    assertTrue(name.endsWith("SECONDS"));
}

@MethodSource

@MethodSourceでは、テストクラスか外部クラスのファクトリーメソッドを1つ以上使うことができます。ファクトリーメソッドは、StreamIterableIterator、または引き通の配列を返す必要があります。さらに、ファクトリーメソッドは、引数を受けてはいけません。テストクラス内のファクトリーメソッドは、テストクラスに@TestInstance(Lifecycle.PER_CLASS)が付与されていない限り、staticである必要があります;一方、外部クラスのファクトリーメソッドは常にstaticである必要があります。

パラメータが1つだけ必要な場合は、次の例が示しているように、パラメータの型のインスタンスのStreamを返すことができます。

@ParameterizedTest
@MethodSource("stringProvider")
void testWithSimpleMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> stringProvider() {
    return Stream.of("foo", "bar");
}

@MethodSourceを通して明示的にファクトリーメソッドの名前を提供しない場合、JUnit Jupiterは、慣習で現在の@ParameterizedTestと同じ名前を持つファクトリーメソッドを探します。これを次の例で示します。

@ParameterizedTest
@MethodSource
void testWithSimpleMethodSourceHavingNoValue(String argument) {
    assertNotNull(argument);
}

static Stream<String> testWithSimpleMethodSourceHavingNoValue() {
    return Stream.of("foo", "bar");
}

DoubleStreamIntStreamLongStreamといったプリミティブ型のStreamもまた、次の例のようにサポートされています。

@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
    assertNotEquals(9, argument);
}

static IntStream range() {
    return IntStream.range(0, 20).skip(10);
}

テストメソッドが複数のパラメータを宣言している場合、下に示すようにArgumentsのコレクションかストリームを返す必要があります。Arguments.of(Object...)は、Argumentsインターフェイスで定義されているstaticなファクトリーメソッドです。

@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
    assertEquals(3, str.length());
    assertTrue(num >=1 && num <=2);
    assertEquals(2, list.size());
}

static Stream<Arguments> stringIntAndListProvider() {
    return Stream.of(
        Arguments.of("foo", 1, Arrays.asList("a", "b")),
        Arguments.of("bar", 2, Arrays.asList("x", "y"))
    );
}

外部のstaticファクトリーメソッドは、次の例で示すように完全修飾メソッド名によって参照されます。

package example;

import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

class ExternalMethodSourceDemo {

    @ParameterizedTest
    @MethodSource("example.StringsProviders#blankStrings")
    void testWithExternalMethodSource(String blankString) {
        // test with blank string
    }
}

class StringsProviders {

    static Stream<String> blankStrings() {
        return Stream.of("", " ", " \n ");
    }
}

@CsvSource

@CsvSourceは、引数リストをCSVとして表現できるようにします(つまり、Stringリテラルです)。

@ParameterizedTest
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
void testWithCsvSource(String first, int second) {
    assertNotNull(first);
    assertNotEquals(0, second);
}

@CsvSourceは、シングルクォーテーション'を引用文字として使います。上の例と下の表の'baz, qux'の値をご覧ください。引用された空の値''は、空のStringとなります;一方、完全にの値はnull参照として解釈されます。null参照のターゲット値がプリミティブ型の場合、ArgumentConversionExceptionが投げられます。

Example Input Resulting Argument List
@CsvSource({ "foo, bar" }) "foo", "bar"
@CsvSource({ "foo, 'baz, qux'" }) "foo", "baz, qux"
@CsvSource({ "foo, ''" }) "foo", ""
@CsvSource({ "foo, " }) "foo", null

@CsvFileSource

@CsvFileSourceは、CSVファイルをクラスパスから使えるようにします。CSVファイルの各行がパラメータテストの1呼び出しに相当しています。

@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSource(String first, int second) {
    assertNotNull(first);
    assertNotEquals(0, second);
}
two-column.csv
Country, reference
Sweden, 1
Poland, 2
"United States of America", 3

:information_source: @CsvSourceで使われているシンタックスとは対照的に、@CsvFileSourceでは引用文字としてダブルクォーテーション"を使います。上記の例の"United States of America"をご覧ください。引用された空の値””は、空のStringとなります;一方、完全にの値はnull参照として解釈されます。null参照のターゲット値がプリミティブ型の場合、ArgumentConversionExceptionが投げられます。

@ArgumentSource

@ArgumentSourceはカスタムの再利用可能なArgumentsProviderを特定するために使うことができます。

@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
    assertNotNull(argument);
}

public class MyArgumentsProvider implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of("foo", "bar").map(Arguments::of);
    }
}

引数変換

広げる変換

JUnit Jupiterは@ParamterizedTestに供給する引数のために、広げるプリミティブ変換をサポートしています。例えば、@ValueSource(ints = { 1, 2, 3 })が付与されたパラメータ化テストは、int型のみならず、longfloatdouble型の引数も受けることができます。

暗示的な変換

@CsvSourceのようなユースケースをサポートするために、JUnit Jupiterは多くの暗示的なビルトイン型変換を提供しています。変換プロセスは、各メソッドパラメータの宣言された型に依存します。

例えば、もし@ParameterizedTestTimeUnit型のパラメータを宣言していて、ソースから供給された実際の型がStringであった場合、Stringは自動的に対応するTimeUnitenum定数に変換されます。

@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(TimeUnit argument) {
    assertNotNull(argument.name());
}

Stringインスタンスは、現在次のターゲット型に暗示的に変換されます。

ターゲット型
boolean/Boolean "true"true
byte/Byte "1" → (byte) 1
char/Character "o"'o'
short/Short "1" → (short) 1
int/Integer "1"1
long/Long "1"1L
float/Float "1"1.0f
double/Double "1"1.0d
Enumサブクラス "SECONDS"TimeUnit.SECONDS
java.io.File "/path/to/file"new File("path/to/file")
java.math.BigDecimal "123.456e789"new BigDecimal("123.456e789")
java.math.BigInteger "1234567890123456789"new BigInteger("1234567890123456789")
java.net.URI "http://junit.org/"URI.create("http://junit.org/")
java.net.URL "http://junit.org/"new URL("http://junit.org/")
java.nio.file.Path "/path/to/file"Paths.get("/path/to/file")
java.time.Instant "1970-01-01T00:00:00Z"Instant.ofEpochMilli(0)
java.time.LocalDateTime "2017-03-14T12:34:56.789"LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)
java.time.LocalDate "2017-03-14"LocalDate.of(2017, 3, 14)
java.time.LocalTime "12:34:56.789"LocalTime.of(12, 34, 56, 789_000_000)
java.time.OffsetDateTime "2017-03-14T12:34:56.789Z"OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.time.OffsetTime "12:34:56.789Z"OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.time.YearMonth "2017-03"YearMonth.of(2017, 3)
java.time.Year "2017"Year.of(2017)
java.time.ZonedDateTime "2017-03-14T12:34:56.789Z"ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.util.Currency "JPY"Currency.getInstance("JPY")
java.util.Locale "en"new Locale("en")
java.util.UUID "d043e930-7b3b-48e3-bdbe-5a3ccfb833db"UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db")

String-to-Object変換のフォールバック

Stringから上のリストで挙げたターゲット型への暗示的な変換に加えて、JUnit Jupiterでは、Stringをある型への自動変換に対するフォールバック機能を提供します。それは目標とする、ターゲット型が、下の定義にまさに正確に合致したファクトリーメソッドファクトリーコンストラクタを宣言するときです。

  • ファクトリーメソッド:ターゲットタイプの中で宣言されているnon-private、かつstaticなメソッドで、1つのString引数を取り、ターゲット型のインスタンスを返すもの。メソッド名は任意であり、いかなる慣習にも従う必要はありません。
  • ファクトリーコンストラクタ:ターゲット型のnon-privateなコンストラクタで、1つのString引数を取るもの。

:information_source: もし複数のファクトリーメソッドが見つかった場合、それらは無視されます。もしファクトリーメソッドファクトリーコンストラクタが見つかった場合、ファクトリーメソッドが、コンストラクタの代わりに使われます。

例えば、次の@ParameterizedTestメソッドの中で、引数BookBook.fromTitle(String)ファクトリーメソッドが呼び出されることで生成され、"42 Cats"が本のタイトルとして渡されます。

@ParameterizedTest
@ValueSource(strings = "42 Cats")
void testWithImplicitFallbackArgumentConversion(Book book) {
    assertEquals("42 Cats", book.getTitle());
}

public class Book {

    private final String title;

    private Book(String title) {
        this.title = title;
    }

    public static Book fromTitle(String title) {
        return new Book(title);
    }

    public String getTitle() {
        return this.title;
    }
}

明示的な変換

暗黙的な引数変換の代わりに、次の例のように@ConvertWithを使うことで、あるパラメータに対して明示的にArgumentConverterを特定することができます。

@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithExplicitArgumentConversion(
        @ConvertWith(ToStringArgumentConverter.class) String argument) {

    assertNotNull(TimeUnit.valueOf(argument));
}

public class ToStringArgumentConverter extends SimpleArgumentConverter {

    @Override
    protected Object convert(Object source, Class<?> targetType) {
        assertEquals(String.class, targetType, "Can only convert to String");
        return String.valueOf(source);
    }
}

明示的な引数変換は、テストと拡張の著者らによって実装される必要があります。そのため、junit-jupiter-paramsでは、レファレンス実装として使える明示的な引数変換器:JavaTimeArgumentConverterを提供しています。結合アノテーションJavaTimeConversionPatternアノテーションを通して使うことができます。

@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithExplicitJavaTimeConverter(
        @JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {

    assertEquals(2017, argument.getYear());
}

引数集約

デフォルトでは、@ParameterizedTestメソッドに渡される各引数は、1つのメソッドパラメータに対応しています。その結果として、大量の引数を供給することが期待される引数ソースは、巨大なメソッドシグネチャーを導くことがあり得ます。

そのような場合、ArgumentAccessorは、複数のパラメータの代わりに使うことができます。このAPIを使うことで、テストメソッドに渡された1つのパラメータを通して提供された引数にアクセスすることができます。さらに、暗黙的な変換で述べた型変換もサポートしています。

@ParameterizedTest
@CsvSource({
    "Jane, Doe, F, 1990-05-20",
    "John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
    Person person = new Person(arguments.getString(0),
                               arguments.getString(1),
                               arguments.get(2, Gender.class),
                               arguments.get(3, LocalDate.class));

    if (person.getFirstName().equals("Jane")) {
        assertEquals(Gender.F, person.getGender());
    }
    else {
        assertEquals(Gender.M, person.getGender());
    }
    assertEquals("Doe", person.getLastName());
    assertEquals(1990, person.getDateOfBirth().getYear());
}

ArgumentsAccessorのインスタンスは、自動的に全てのArgumentsAccessor型のパラメータに注入されます

カスタム集約

ArgumentsAccessorを用いた@ParameterizedTestメソッドの引数への直接アクセスとは別に、JUnit Jupiterはカスタムで再利用可能な集約器の使用もサポートしています。

カスタム集約器を使うためには、単にArgumentAggregatorインターフェイスを実装し、@ParameterizedTestメソッド内で互換可能なパラメータに対して、@AggregateWithを付与して登録するだけです。集約の結果は、パラメータ化テストが呼び出された時に、対応するパラメータへの引数として提供されます。

@ParameterizedTest
@CsvSource({
    "Jane, Doe, F, 1990-05-20",
    "John, Doe, M, 1990-10-22"
})
void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {
    // perform assertions against person
}

public class PersonAggregator implements ArgumentsAggregator {
    @Override
    public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) {
        return new Person(arguments.getString(0),
                          arguments.getString(1),
                          arguments.get(2, Gender.class),
                          arguments.get(3, LocalDate.class));
    }
}

もし反復して複数のパラメータ化テストに対して@AggregateWith(MyTypeAggregator.class)を宣言している場合、@AggregateWith(MyTypeAggregator.class)のメタアノテーションとして@CsvToMyTypeのようなカスタム結合アノテーションを作ることができます。次の例は、カスタム@CsvToPersonアノテーションを用いた例を示しています。

@ParameterizedTest
@CsvSource({
    "Jane, Doe, F, 1990-05-20",
    "John, Doe, M, 1990-10-22"
})
void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
    // perform assertions against person
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}

表示名のカスタマイズ

デフォルトでは、パラメータ化テスト呼び出しの表示名は、呼び出しインデックスと呼び出しに対する全ての引数のString表現を含んでいます。しかしながら、次の例のように@ParameterizedTestアノテーションのname属性によって呼び出し表示名をカスタマイズすることができます。

@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> first=''{0}'', second={1}")
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
void testWithCustomDisplayNames(String first, int second) {
}

上記のメソッドをConsoleLauncherを使って実行すると、次のような出力が表示されます。

Display name of container ✔
├─ 1 ==> first='foo', second=1 ✔
├─ 2 ==> first='bar', second=2 ✔
└─ 3 ==> first='baz, qux', second=3 ✔

カスタム表示名では、次のプレースホルダがサポートされています。

プレースホルダ 説明
{index} 現在の呼び出しインデックス(1始まり)
{arguments} 完全な引数リスト(CSV形式)
{0}, {1}, ... 各引数

ライフサイクルと相互運用性

パラメータ化テストの各呼び出しは、通常の@Testメソッドと同じライフサイクルを持っています。例えば、各呼び出し前には@BeforeEachメソッドが実行されます。動的テストと同じように、呼び出しはIDEのテストツリーでは一つ一つ表れます。同一のテストクラスに、自由に@Test@ParameterizedTestを混ぜることができます。

@ParameterizedTestメソッドと合わせてParameterResolverを使うことができます。しかしながら、引数ソースによって解決されたパラメータは、引数リストの最初に来る必要があります。テストクラスは様々なパラメータリストを持つパラメータ化テストと同様に通常のテストクラスを含んでいることもあるので、引数ソースからの値は、@BeforeEachといったライフサイクル・メソッドやテストクラスコンストラクタは解決されません。

@BeforeEach
void beforeEach(TestInfo testInfo) {
    // ...
}

@ParameterizedTest
@ValueSource(strings = "foo")
void testWithRegularParameterResolver(String argument, TestReporter testReporter) {
    testReporter.publishEntry("argument", argument);
}

@AfterEach
void afterEach(TestInfo testInfo) {
    // ...
}

テストテンプレート

@TestTemplateメソッドは、通常のテストケースではなく、むしろテストケースのためのテンプレートです。したがって、@TestTemplateは、登録された提供器によって返される呼び出し文脈の数に応じて複数回呼び出されるものとして設計されています。そのため、登録されたTestTemplateInvocationContextProvider 拡張と併せて使われる必要があります。テストテンプレートメソッドの各呼び出しは、通常の@Testメソッドの実行と同じように振る舞い、全く同じライフサイクル・コールバックと拡張が完全にサポートされています。用法例については、テストテンプレートに対する呼び出し文脈の提供を参照ください。

動的テスト

アノテーションで説明したJUnit Jupiterの@Testアノテーションは、JUnit 4の@Testアノテーションに非常に似通っています。どちらもテストケースを実装したメソッドを描写します。これらのテストケースはコンパイル時に完全に決定するという意味では静的であり、それらの振る舞いは実行時に変更することはできません。アサンプションは、意図的にかなり表現性に制限のあるものですが、動的な振る舞いの基本的な形式を提供します

これらの標準的なテストに加えて、全く新しい種類のテストプログラミング・モデルがJUnit Jupiterでは導入されました。この新しいテストとは、動的テストです。動的テストは、@TestFactoryが付与されたファクトリーメソッドによって、実行時に生成されます。

@Testメソッドとは対照的に、@TestFactoryメソッド自身はテストケースではなく、むしろテストケースのためのファクトリーです。そのため、動的テストはファクトリーの産出物となります。技術的なことを言うと、@TestFactoryメソッドは、DynamicNodeインスタンスのStreamCollectionIterable、もしくはIteratorを返さなければなりません。DynamicNodeのインスタンス化可能なサブクラスはDynamicContainerDynamicTestです。DynamicContainerインスタンスは、表示名と動的子ノードリストで構成されており、動的ノードの任意なネスト階層を生成します。DynamicTestインスタンスは、遅れて実行され、テストケースを動的で非決定的に生成します。

@TestFactoryによって返されるStreamはどれも、stream.close()を呼ぶことによって適切に閉じられます。これによって、Files.lines()のような資源を安全に使うことができます。

@Testメソッドと同様に、@TestFactoryメソッドもprivatestaticでいることはできず、任意にParameterResolversで解決されるであろうパラメータを宣言することができます。

DynamicTestは実行時に生成されるテストケースで、表示名Executableで構成されています。Executable@FunctionalInterface`で、このインターフェイスは、ラムダ表現メソッド参照として提供されることができる動的テストの実装であることを意味します。

:no_entry_sign: 動的テスト・ライフサイクル:動的テストの実行ライフサイクルは、通常の@Testケースとは全く異なります。特に、各動的テストに対してのライフサイクル・コールバックはありません。このことは、@BeforeEach@AfterEach、それに対応した拡張コールバックは@TestFactoryメソッドに対して実行され、各動的テストには実行されないことを意味します。つまり、動的テストに対してラムダ表現でテストインスタンスからフィールドにアクセスしても、それらのフィールドは、同じ@TestFactoryメソッドで生成された動的テストの実行中は、コールバックメソッドやその拡張によってリセットされません。

JUnit Jupiter 5.2.0-M1に関しては、動的テストは常にファクトリーメソッドによって生成される必要があります;しかしながら、これは後のリリースの登録機能によって補完されるかもしれません。

:no_entry_sign: 動的テストは現在実験的な機能です。詳細に関しては、Experimental APIsをご覧ください。

動的テストの例

次のDynamicTestsDemoクラスは、テストファクトリーと動的テストのいくつかの例を示しています。

最初のメソッドは不正な型を返しています。不正な返却型はコンパイル時に検出することができないため、実行時に検出されJUnitExceptionが投げられます。

次の5つもメソッドは、DynamicTestインスタンスのCollectionIterableIteratorStreamを生成する非常に単純な例です。これらの例のほとんどは、実際に動的振る舞いを示しておらず、単に原則的にサポートされている返却型を示しています。しかしながら、dynamicTestsFromStream()dynamicTestsFromIntStream()は、与えられたStringのセットと入力された数の範囲に対する動的テストの生成が、いかに簡単かを示しています。

次のメソッドは、性質上、真に動的なものです。generateRandomNumberOfTests()は、ランダム数を生成するIteratorと表示名生成器、テスト実行器を実装しており、その3つをDynamicTest.stream()に提供しています。generateRandomNumberOfTests()の非決定的な振る舞いは、もちろんテスト反復可能性に抵触しており、注意深く取り扱われるべきではありますが、動的テストの表現性と力を示しています。

最後のメソッドは、DynamicContainerを使って動的テストのネスト階層を生成しています。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;

class DynamicTestsDemo {

    // This will result in a JUnitException!
    @TestFactory
    List<String> dynamicTestsWithInvalidReturnType() {
        return Arrays.asList("Hello");
    }

    @TestFactory
    Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(
            dynamicTest("1st dynamic test", () -> assertTrue(true)),
            dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
        );
    }

    @TestFactory
    Iterable<DynamicTest> dynamicTestsFromIterable() {
        return Arrays.asList(
            dynamicTest("3rd dynamic test", () -> assertTrue(true)),
            dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2))
        );
    }

    @TestFactory
    Iterator<DynamicTest> dynamicTestsFromIterator() {
        return Arrays.asList(
            dynamicTest("5th dynamic test", () -> assertTrue(true)),
            dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))
        ).iterator();
    }

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStream() {
        return Stream.of("A", "B", "C")
            .map(str -> dynamicTest("test" + str, () -> { /* ... */ }));
    }

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromIntStream() {
        // Generates tests for the first 10 even integers.
        return IntStream.iterate(0, n -> n + 2).limit(10)
            .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
    }

    @TestFactory
    Stream<DynamicTest> generateRandomNumberOfTests() {

        // Generates random positive integers between 0 and 100 until
        // a number evenly divisible by 7 is encountered.
        Iterator<Integer> inputGenerator = new Iterator<Integer>() {

            Random random = new Random();
            int current;

            @Override
            public boolean hasNext() {
                current = random.nextInt(100);
                return current % 7 != 0;
            }

            @Override
            public Integer next() {
                return current;
            }
        };

        // Generates display names like: input:5, input:37, input:85, etc.
        Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;

        // Executes tests based on the current input value.
        ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);

        // Returns a stream of dynamic tests.
        return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
    }

    @TestFactory
    Stream<DynamicNode> dynamicTestsWithContainers() {
        return Stream.of("A", "B", "C")
            .map(input -> dynamicContainer("Container " + input, Stream.of(
                dynamicTest("not null", () -> assertNotNull(input)),
                dynamicContainer("properties", Stream.of(
                    dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
                    dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
                ))
            )));
    }

}
68
73
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?