JUnit5（概要〜チュートリアル）

# はじめに
Junit5のユーザーガイドをベースにテストコードを書きました。
内容は、概要把握とチュートリアルレベルになります。

# 実行環境
OS：Mac
JUnit：JUnit5.8.2
Java：Amazon Corretto 8
Gradle：Gradle6.8
IDE：IntelliJ IDEA 2022.1 Community

# Junit5とは
現在リリースされている最新のJUnitです。
JUnit5は大きく分けて以下のサブプロジェクトから構成されています。

以下、ユーザーガイドより

## JUnit Platform
JVM上でテストフレームワークを起動するための基盤です。
このプラットフォーム上で動作するテストフレームワークを開発するためのTestEngine APIが定義されています。
加えて、コマンドラインからプラットフォームを起動するためのConsole Launcherや、
GradleとMaven用のビルドプラグイン、 JUnit 4ベースのテストランナーなどが提供されており、
あらゆるTestEngineを実行できるようになっています。

## JUnit Jupiter
JUnit5でテストや拡張機能を書くための新しいプログラミングモデルと拡張モデルの組み合わせです。
Jupiterサブプロジェクトは、プラットフォーム上でJupiterベースのテストを実行するためのTestEngineを提供します。

## JUnit Vintage
プラットフォーム上でJUnit 3またはJUnit 4ベースのテストを実行するためのTestEngineを提供します。

# 対応するJavaバージョン
Java 8以上を対象としていますが、古いバージョンのJDKでコンパイルされたコードをテストすることも可能とされています。

# 導入準備
## Gradleに依存関係を追加する
[こちらのページ](https://github.com/junit-team/junit5-samples/blob/main/junit5-jupiter-starter-gradle/build.gradle)を参考に依存関係を追加します。

```groovy
dependencies {
    ・
    ・
    testImplementation platform('org.junit:junit-bom:5.8.2')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

```

# テストを書く
Junitではテストするクラスに対応するテストクラスを作成します。
そのテストクラス内にテストケースとなるメソッドを実装していきます。

## テストクラスとは
テストメソッドを1つ以上持つ、トップレベルまたはstaticな内部クラスのことを指します。

## テストメソッドとは
以下のいずれかのアノテーションを付与されたインスタンスメソッドのことを指します。

| アノテーション | 詳細 |
|-------|-------|
| @&#xFFF0;Test　| 付与したメソッドをテストメソッドとして扱います。|
| @&#xFFF0;RepeatedTest　| 付与したメソッドを繰り返しテストのメソッドとして扱います。 |
| @&#xFFF0;ParameterizedTest　| 付与したメソッドをパラメーター化テストのメソッドとして扱います。 |
| @&#xFFF0;TestFactory　| 付与したメソッドを動的テスト用のテストファクトリとして扱います。 |
| @&#xFFF0;TestTemplate　| 付与したメソッドをテストケースのテンプレートとして扱います。<br/>登録したプロバイダが返す実行コンテキストの数に応じて、複数回呼び出されます。 |

上記を直接付与するだけでなく、継承した独自アノテーションも利用出来ます。
詳しくは[こちら](https://oohira.github.io/junit5-doc-jp/user-guide/#writing-tests-meta-annotations)

## 主要なアノテーション
よく使用することになるアノテーションは[こちら](https://oohira.github.io/junit5-doc-jp/user-guide/#writing-tests-annotations)に説明があります。
主要なアノテーション類はjunit-jupiter-apiモジュールの「org.junit.jupiter.api」パッケージに用意されています。

## サンプルを実装
テストコードを実装しました。
実行確認だけなのでテスト対象のクラスなどはなく、単純に「1 + 1 = 2」をテストしています。

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

import org.junit.jupiter.api.Test;

public class FirstJUnit5Tests {

    @Test
    void myFirstTest() {
        // 第1引数の期待値と第2引数の実際の値が等しいかチェック
        assertEquals(2, 1 + 1);
    }
}
```

テスト用に作成したFirstJUnit5Testsクラスにテストケースとなるメソッドを作成します。
作成したメソッドに@Testを付与するとテストコードとして実行出来ます。

## 繰り返しテスト
@RepeatedTestを使ってテストメソッドを宣言します。
@RepeatedTestの引数に指定した回数だけ繰り返しテストを実行します。
繰り返しテストでは、指定された回数毎にライフサイクルコールバックや拡張機能など、
通常の@Testメソッドと同様に実行します。

以下の例では、テストケースを10回繰り返します。

```java
@RepeatedTest(10)
void repeatedTest() {
    assertEquals(2, 1 + 1);
}
```

## パラメーター化テスト
@ParameterizedTestを使ってテストメソッドを宣言します。
@ValueSourceで引数を宣言し、引数の数だけテストを複数回実行します。

以下の例では、apple, orangeという引数を@ValueSourceを使って渡します。
渡ってきた引数はフルーツリストに含まれていることをテストしています。

```java
@ParameterizedTest
@ValueSource(strings = { "apple", "orange"})
void parameterizedTest(String fruit) {
    List<String> fruits = new ArrayList<String>(Arrays.asList("apple", "orange", "melon"));
    assertTrue(fruits.contains(fruit));
}
```

より詳しくは[こちら](https://oohira.github.io/junit5-doc-jp/user-guide/#writing-tests-parameterized-tests)

## 動的テスト
これまで@Test（Junit4の@Testも含め）などのアノテーションを使ってテストケースを作成してきました。
これらはコンパイル時に完全に指定され、実行時に振る舞いが変えられない特性があるため、静的なテストと位置付けられています。

JUnit Jupiterでは上記に加え、動的テストが導入されています。
動的テストは、@TestFactoryを付与したファクトリメソッドによって実行時に生成されます。

### 実装例

@TestFactoryを付与したメソッド自身はテストケースではなく、
テストケースのファクトリで、そのメソッドの返却値が動的テストになります。
そのため、@TestFactoryを付与したメソッドは、DynamicNodeインスタンスの
Stream, Collection, Iterable, Iterator, 配列のいずれかを返すように
実装する必要があります。

DynamicNodeをインスタンス化するにはDynamicContainerとDynamicTestの2つのサブクラスがあります。
DynamicContainerインスタンスは、表示名と子どものDynamicNodeリストで構成されます。

```java
@TestFactory
Collection<DynamicNode> dynamicTestsFromCollection() {
    // DynamicTestを格納したリストを返します
    return Arrays.asList(
        // リスト内のDynamicTestクラスでテストケースを表現します
        dynamicTest("1st dynamic test", () -> assertTrue(true)),
        dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
    );
}

@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {
    // リテラルのA,B,Cを持つストリームを生成する
    return Stream.of("A", "B", "C")
        // 要素のA,B,CそれぞれにDynamicContainerを2つ用意し、それぞれテストします。
        // 【コンテナ1】
        // 　・テスト1 Null出ないことをテスト
        // 【コンテナ2】
        // 　・テスト1 リテラルの長さが0以上であることをテスト
        // 　・テスト2 リテラルが空であることをテスト
        .map(input -> dynamicContainer("Container " + input, Stream.of(
            dynamicTest("not null", () -> assertNotNull(input)),
            // DynamicContainerには表示名とテストケースとなるDynamicNodeリストを指定します。
            dynamicContainer("properties", Stream.of(
                dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
                dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
            ))
        )));
}
```

## テストケースのテンプレート
@TestTemplateを付与することでテストケースのテンプレートメソッドを実装出来ます。
@ExtendWithで指定したプロバイダが返す実行コンテキストの数だけ呼び出されます。
そのため、TestTemplateInvocationContextProviderが少なくとも1つ登録されている必要があります。
テストテンプレートメソッドの各呼び出しは、ライフサイクルコールバックや拡張機能など、
@Testを付与したメソッドの実行と同じように振る舞います。


以下は、テストケースに渡ってきた引数の長さが3であることをテストしています。
その際、渡ってくる引数をMyTestTemplateInvocationContextProviderでテンプレート化した例になります。

```java
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.*;

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

public class FirstJUnit5Tests {

    @TestTemplate
    @ExtendWith(MyTestTemplateInvocationContextProvider.class)
    void testTemplate(String parameter) {
        assertEquals(3, parameter.length());
    }

}
```

@ExtendWithで指定するプロバイダは、TestTemplateInvocationContextインスタンスのStreamを返す必要があります。
各コンテキストは、カスタム表示名と @TestTemplateメソッドの次の呼び出しだけで使われる、追加の拡張機能のリストを指定します。

以下のプロバイダでは、テストケースで使用する引数を定義し、コンテキストを回してテストケースの引数へと渡します。

```java
import org.junit.jupiter.api.extension.*;

import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

/**
 * TestTemplateInvocationContextProviderインターフェースを実装したプロバイダ
 */
public class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider {

    /**
     * このプロバイダが、テストテンプレートメソッドの呼び出しコンテキストをサポートしているか判定します。
     */
    @Override
    public boolean supportsTestTemplate(ExtensionContext context) {
        return true;
    }

    /**
     * テストテンプレートメソッドの呼び出しコンテキストを返します。
     * このメソッドは、supportsTestTemplateメソッドがtrueを返した場合にのみ、フレームワークから呼び出されます。
     * ※このメソッドは空のStreamを返すことができますが、nullは返すことができません。
     */
    @Override
    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
        return Stream.of(invocationContext("foo"), invocationContext("bar"));
    }

    /**
     * コンテキストを呼び出します。
     * コンテキストはTestTemplateInvocationContextクラスを生成します。
     * コンテキストには表示名、追加の拡張機能のリストをオーバーライドして実装します。
     */
    private TestTemplateInvocationContext invocationContext(String parameter) {
        return new TestTemplateInvocationContext() {

            // 表示名を返すメソッドです。
            // テストを実行した時にIDEAのディスプレイ上に表示されます。
            @Override
            public String getDisplayName(int invocationIndex) {
                return parameter;
            }

            // 追加する拡張機能リストを返します。
            @Override
            public List<Extension> getAdditionalExtensions() {

                // 以下では、ParameterResolverクラスで拡張機能を定義しています。
                // ParameterResolverクラスではsupportsParameterメソッドとresolveParameterを実装する必要があります。
                return Collections.singletonList(new ParameterResolver() {

                    // コンテキストの呼び出しで指定された引数を、このリゾルバがサポートするものであるか判定します。
                    @Override
                    public boolean supportsParameter(ParameterContext parameterContext,
                                                     ExtensionContext extensionContext) {
                        return parameterContext.getParameter().getType().equals(String.class);
                    }

                    // テストケースに渡す引数に対して処理を実装します。
                    // このメソッドはsupportsParameterメソッドの判定がtrueの場合にみ呼び出されます。
                    // 以下では、何もしないのでコンテキストの呼び出しで指定された文字列をそのまま返します。
                    @Override
                    public Object resolveParameter(ParameterContext parameterContext,
                                                   ExtensionContext extensionContext) {
                        return parameter;
                    }
                });
            }
        };
    }
}
```

# 最後に
今回、ユーザーガイドを通して学んだことは以上になります。
JUnit5には他にも多くの機能がありますので、また追々触っていこうと思います。

#参考文献

https://oohira.github.io/junit5-doc-jp/user-guide/