java.svg

【Java】JUnit 5によるテストの基本

Java

導入

参考:JUnit 5 User Guide

JUnit は、Java におけるユニットテストのためのフレームワークです。 この記事では JUnit 5 を対象にしています。

JUnit 5 は、以下の 3 つのサブプロジェクトに含まれる複数のモジュールで構成されます。

  • JUnit Platform:JVM(Java 仮想マシン)上でテストフレームワークを起動するための基盤
  • JUnit Jupiter:JUnit 5 でテストや拡張機能を作成するためのモジュール
  • JUnit Vintage:JUnit 3, 4 を実行するためのもの

新規で導入する場合は、JUnit Vintage は不要のため、JUnit Platform と JUnit Jupiter を導入します。

Maven

Maven プロジェクトの場合は、pom.xmlに以下を追加します。

pom.xml
<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.8.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.2</version>
        </plugin>
        <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>2.22.2</version>
        </plugin>
    </plugins>
</build>

Gradle

Gradle プロジェクトの場合、build.gradleに以下を追加します。

bluild.gradle
repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
}

test {
    useJUnitPlatform()
}

repositoriesは、Maven のセントラルリポジトリを参照するようにmavenCentral()を指定します。 JCenter()となっている場合は、修正してください。

テストの実行

まずは簡単なテストを作成してみます。

引数の値を基に「Hello XXX!!」という文字列を返すメソッド(greet())を作成しました。

Hello.java
public class Hello {
  public String greet(String name) {
    return String.format("Hello %s!!", name);
  }
}

これが正しく動作するかを以下のテストコードで検証します。

HelloTest.java
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

public class HelloTest {

  Hello hello = new Hello();

  @Test
  void あいさつのテスト() {
    String s = hello.greet("Tanaka");
    assertEquals(s, "Hello Tanaka!!");
  }
}

@Testが付与されたメソッドに、テストで検証する処理を記述します。 メソッド名は日本語でも OK です。 ここでは 1 つしか定義していませんが、複数定義することが可能です。

実際の値の検証には、assertEquals()を使用します。 第一引数と第二引数の値が等しい場合はテスト成功、異なる場合はテスト失敗となります。 値が等しいというのは、equals()メソッドがtrueとなることを指します。 その他の検証方法については後述します。

Eclipse の場合だと、テスト用のファイルで右クリック > 実行 > JUnit テストを選択することでテストを実行することができます。 プロジェクト内のすべてのテストを実行したい場合は、プロジェクトを右クリック > 実行 > JUnit テストを選択します。

結果は、以下の画像のように成功、失敗、エラーの 3 つのステータスで表示されます。 成功はassertEquals()で値が等しいと判断された場合、失敗は値が異なった場合、エラーは例外が発生した場合を意味します。

  • 成功の例java-junit1
  • 失敗の例java-junit2

テストパターンがしっかり作れており、すべてのステータスが成功であれば、ある程度の品質が担保されるということになります。 逆に失敗やエラーがある場合は、バグや仕様が違うことを疑います。

検証方法の種類

上記では値の検証にassertEquals()を使用しましたが、他に以下のようなものがあります。 比較する値(引数)の型ごとにメソッドが用意されていますが、すべてを記載するのは大変なため簡略化しています。 詳細はこちらを確認してください。

※以下は JUnit 5.8.1 を対象としています。バージョンの違いにより使用できないものがあります。

メソッド説明
assertEquals(x, y)xyが等しい(equals() == trueである)ことを検証
assertNotEquals(x, y)xyが等しくない(equals() == falseである)ことを検証
assertArrayEquals(x, y)配列xyのすべての要素が順番通りに等しい(equals() == trueである)ことを検証
assertIterableEquals(x, y)IteratableListなど)なxyのすべての要素が順番通りに等しい(equals() == trueである)ことを検証
assertNotNull(x)xnullでないことを検証
assertTrue(x)xtrueであることを検証
assertFalse(x)xfalseであることを検証
assertSame(x, y)xyが同一オブジェクト(x == yである)ことを検証
assertNotSame(x, y)xyが異なるオブジェクト(x != yである)ことを検証
assertInstanceOf(c, x)xが指定したクラスcのオブジェクトであることを検証
assertLinesMatch(x, y)yxで指定したパターンのListまたはStreamであることを検証
assertThrows(e, f)処理fが例外クラスeまたはそのサブクラスをスローすることを検証
assertThrowsExactly(e, f)処理fが例外クラスeをスローすることを検証
assertDoesNotThrow(f)処理fが例外クラスをスローしないことを検証
assertTimeout(t, f)処理fが指定した時間t内に終わることを検証
処理は最後まで実行される
assertTimeoutPreemptively(t, f)処理fが指定した時間t内に終わることを検証
処理はタイムアウトになった時点で終了する

上記の内いくつかは、記載方法を含めて補足していきます。

assertLinesMatch

以下はassertLinesMatchの例です。 第一引数には、各要素のパターンを正規表現を使って指定します。

String[] x = {"^[a-z]{1}$", "^[0-9]+$", "a"};
String[] y = {"b", "905", "a"};
assertLinesMatch(Arrays.asList(x), Arrays.asList(y));

各要素は、Stringmatches()メソッドによって判定され、 すべての要素がtrueの場合に成功となります。

一部の要素のみ判定したい場合は、">> >>"を使用することで判定をスキップすることができます。 以下の例では、最初と最後の要素のみ判定を行います。

String[] x = {"^[a-z]{1}$", ">> >>", "a"};
String[] y = {"b", "905", "Af", "a"};
assertLinesMatch(Arrays.asList(x), Arrays.asList(y));

assertThrows

以下はassertThrowsの例です。 処理(メソッド)は、ラムダ式を用いて指定します。

class Hoge {
  public fuga() {
    //...
    throw new RuntimeException();
  }
}
assertThrows(Exception.class, () -> { hoge.fuga(); });  //成功

上述したように、指定した例外クラスまたはそのサブクラスであれば成功となります。

assertThrowsExactlyの場合は、指定した例外クラスのみ成功となります。 サブクラスでは失敗となります。

assertThrowsExactly(Exception.class, () -> { hoge.fuga(); });  //失敗

assertTimeout

以下はassertTimeoutの例です。 処理(メソッド)は、ラムダ式を用いて指定します。

タイムアウトまでの時間は、Durationを使用します。 Duration.ofSeconds()のように時間単位を指定して設定します。

assertTimeout(Duration.ofSeconds(5), () -> { hoge.fuga(); });

タイムアウト時間内に処理が終われば成功、時間を過ぎると失敗となります。

assertTimeoutは、時間を過ぎても処理が終了するまで待ち続けます。 assertTimeoutPreemptivelyは、時間が過ぎた時点で処理が強制的に終了します。

assertAll

複数の検証を行いたい場合、以下のように個別に指定することができます。

@Test
void テスト() {
  assertTrue(hoge);
  assertTrue(fuga);
  assertTrue(piyo);
}

しかし、このように指定すると失敗した時点で処理が終了してしまいます。 上の例でfuga = falseだとすると、piyoは検証されません。

失敗してもすべての検証を実行したい場合は、assertAllを使用します。

@Test
void テスト() {
  assertAll(
    () -> assertTrue(hoge),
    () -> assertTrue(fuga),
    () -> assertTrue(piyo)
  );
}

ライフサイクル

テストクラスでは、事前処理または事後処理として、以下のアノテーションを付与したメソッドを定義します。

  • @BeforeAll:すべてのテストの実行前の処理
  • @AfterAll:すべてのテストの実行後の処理
  • @BeforeEach:各テストの実行前の処理
  • @AfterEach:各テストの実行後の処理

@BeforeAll@AfterAllを付与するメソッドは、staticである必要があります。

各処理の実行順序をイメージするために、以下のコードを実行してみます。 実行結果を見れば、どのような順で処理されているかがわかると思います。

public class SampleTest {

  public SampleTest() {
    System.out.println("constructor");
  }

  @BeforeAll
  static void beforeAll() {
    System.out.println("Before All");
  }

  @AfterAll
  static void afterAll() {
    System.out.println("After All");
  }

  @BeforeEach
  void beforeEach() {
    System.out.println("Before Each");
  }

  @AfterEach
  void afterEach() {
    System.out.println("After Each");
  }

  @Test
  void test1() {
    System.out.println("test1");
  }

  @Test
  void test2() {
    System.out.println("test2");
  }
}
実行結果
Before All
constructor
Before Each
test1
After Each
constructor
Before Each
test2
After Each
After All

ここで注目していただきたいのが、constructorです。 実行結果から@BeforeEach実行前に必ずコンストラクタ―が実行されていることが分かります。

コンストラクタ―の実行を 1 回に限定したい場合は、以下のように@TestInstanceをテストクラスに付与します。

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SampleTest() {
  //略...
}
実行結果
constructor
Before All
Before Each
test1
After Each
Before Each
test2
After Each
After All

実行結果から分かるように、コンストラクタ―の実行が最初のみとなっています。 ちなみにデフォルトでは、TestInstance.Lifecycle.PER_METHODが設定されています。

テストの種類

@Test

これまでにも記述した通り、@Testを付与したメソッドはテストメソッドとなり、テストが実行されます。

@ParameterizedTest

@ParameterizedTest@ValueSourceを付与することで、同じテストを異なる値で繰り返し実行することができます。 以下に、isAlpha()をテストする例を示します。

public isAlpha(String arg) {
  return arg != null && arg.matches("^[a-zA-Z]*$");
}
@ParameterizedTest
@ValueSource(strings = {"a", "abc", "A", "ABCDE"})
void isAlphaTrue(String s) {
  assertTrue(isAlpha(s));
}

@ParameterizedTest
@ValueSource(strings = {"123", "a1", "@*]", "あ"})
void isAlphaFalse(String s) {
  assertFalse(isAlpha(s));
}

@ValueSourceに指定した値の分だけテストが実行されます。 指定した値は、テストメソッドの引数として受け取ります。

nullや空文字をテストに含めたい場合は、@NullSource@EmptySourceを付与します。 どちらも指定したい場合は、@NullAndEmptySourceを付与します。

@ValueSourceについて、ここではstringsとしていますが、intsで整数を指定することなどもできます。 詳細についてはこちらを参照してください。

値の指定については、@ValueSourceの他に@CsvSource@EnumSourceなどがあります。 この記事では説明しませんが、以下に記載がありますので参考にしてみてください。

@RepeatedTest

@RepeatedTestを付与することで、同じテストを指定した回数実行することができます。

@RepeatedTest(5)
void test1() {
  assertTrue(hoge());
}

nameプロパティを指定することで、テスト名を変更することができます。 nameを指定する際、以下は特別な値(変数)として処理されます。

  • {displayName}:もともとのテスト名
  • {currentRepetition}:現在のテスト実行回数
  • {totalRepetitions}:テストの総実行回数
@RepeatedTest(value = 5, name="{displayName: {currentRepetition}/{totalRepetitions}")
void test1() {
  assertTrue(hoge());
}
実行結果
test1: 1/5
test1: 2/5
test1: 3/5
test1: 4/5
test1: 5/5

@TestFactory

@TestFactoryを付与することで、動的にテストを作成することができます。 まずは以下をみてください。

@TestFactory
DynamicTest[] dynamicTest() {
  DynamicTest[] tests = [
    dynamicTest("test1", () -> assertTrue(true)),
    dynamicTest("test2", () -> {
      int x = 2;
      assertEquals(x, 2);
    })
  ];
  return tests;
}

@TestFactoryを使用する場合は、複数のDynamicTestを戻り値にする必要があります。 複数という指定は、上記のように配列を用いる他に、CollectionIterableIteratorStreamが使用できます。

DynamicTestは、第一引数にテスト名を、第二引数にテストの処理を指定します。

Stream API を用いれば、以下のように定義することもできます。

@TestFactory
Iterator<DynamicTest> dynamicTest() {
  List<Sample> samples = new ArrayList<Sample>() {
    private static final long serialVersionUID = 1L;
    {
      add(new Sample(1, "a", "A"));
      add(new Sample(2, "b", "B"));
      add(new Sample(3, "c", "C"));
    }
  };
  return samples.stream()
                .map(e -> dynamicTest("test" + e.getId(), () -> {
                  assertEquals(e.hoge(), "aA");
                }).iterator();
}

その他

@DisplayName

テスト名を設定します。

@DisplayName("テスト名")
void test() {
  //...
}

@Disabled

@Disabledを付与したテストメソッドは、テストの対象外になります。

@Test
@Disabled
void test() {
  //...
}