🧩

Java単体テストにおけるJUnitの導入

2022/11/20に公開

概要

  • JUnitで基本としてよく使うところのメモ
  • バージョン:junit-jupiter-engine 5.6.2
  • Spring Boot + Maven での利用を想定

最初のテスト

JUnit 5 User Guide(公式)のコード

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

import org.junit.jupiter.api.Test;

class FirstJUnit5Tests {

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

}

"2"が想定値、"1 + 1"が実行値でそれを検証する単純な例。実行結果は成功。

以前はpublicをつけていたが、以下の通り、外部パッケージとかで使わなければ基本省略。
https://junit.org/junit5/docs/current/user-guide/#writing-tests-classes-and-methods

ちなみに、assertEqualsの行を消しても、実行結果は成功。
JUnitの基本ルールとして、失敗(Failure)がなければ成功。

アサーション

基本的に"assert~"というメソッドでテストメソッドの主要部。
様々な種類があり、オーバーロードされて用意されている。
https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/Assertions.html
主要なassertメソッド

  • assertAll
  • assertArrayEquals, assertIterableEquals
  • assertEquals, assertNotEquals
  • assertTrue, assertFalse
  • assertNull, assertNotNull
  • assertThrows
  • assertTimeout

複数のassertをグループ化

@Test
void groupedAssertions() {
    // アサーションをグループ化すると、すべてのアサーションが一度に実行され、
    // すべての失敗がまとめて報告される。
    assertAll("person",
        () -> assertEquals("Jane", person.getFirstName()),
        () -> assertEquals("Doe", person.getLastName())
    );
}

assertAllを使えば、アサーションをグループ化して一度に実行し、結果をまとめて返す。
途中のassertで失敗しても次のassertを実行する。
この例で言うと、Janeで失敗してもDoeのassertもやる。

例外のテスト

単純な場合はassertThrowsを使えばよい。

@Test
void throwIOExceptionTest() {
    assertThrows(IOException.class, () -> { throw new IOException("ERROR"); });
}

assertThrowsの第2引数はExecutable型なので、一時的に変数として持っても問題ない。
さらに、返却値は例外クラスなので、自作クラスなどでさらなるチェックも可能。

@Test
void throwMyApplicationExceptionTest() {
    Executable executable = () -> {
      throw new MyApplicationException(new MessageCode("E_1000"), "Application Error!"));
    };

    MyApplicationException e = assertThrows(MyApplicationException.class, executable);
    assertEquals("E_1000", e.getInternalCode());
}

配列のテスト

二つの配列の要素の値が完全一致するかをテストする場合、
生配列はassertArrayEquals、List型はassertIterableEqualsを使えばよい。

@Test
void rawArrayEqualsTest() {
    String[] expected = {"TOKYO", "SAITAMA", "CHIBA"};

    String[] actual = new String[3];
    actual[0] = "tokyo".toUpperCase();
    actual[1] = "saitama".toUpperCase();
    actual[2] = "chiba".toUpperCase();

    assertArrayEquals(expected, actual);
}
@Test
void listEqualsTest() {
    List<Integer> expected = new ArrayList<>() {{ add(1), add(2), add(3) }};

    List<Integer> actual = new ArrayList<>();
    actual.add(1);
    actual.add(2);
    actual.add(3);

    assertIterableEquals(expected, actual);
}

パフォーマンスのテスト

assertTimeoutassertTimeoutPreemptivelyを使えばよい。

@Test
void performanceTest() {
    Executable executable = () -> {
      for (int i = 0; i < 999999999; i++) {
        int a = i * i;
      }
    };

    assertTimeout(Duration.ofMills(50), executable);
}

assertTimeoutはテスト対象の処理が終わるまでずっと待つ。
assertTimeoutPreemptivelyはタイムアウト時間に到達した時点でFailとなる。
タイムアウト時間の指定はjava.time.DurationのofMills, ofSecondsなどでやる。

Parameterized Tests

特定の変数に対して様々な値を入れ替えてテストしたいときに便利。
公式のコードのように、@ValueSourceを使ってパラメータを注入するのが最もシンプルで、
型と候補値をそのまま記述すれば良い。

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

他にもパラメータ注入パターンとしてはいろいろと種類があるので公式ドキュメントを要確認。

@BeforeEach, @AfterEach

それぞれのテストメソッドの前or後に毎回実行したい処理を定義可能。

@BeforeEach
void init() {
}

// このメソッドの処理実行前にinit()を実行
@Test
void succeedingTest() {
}
// このメソッドの処理実行後にtearDown()を実行

// このメソッドの処理実行前にinit()を実行
@Test
void failingTest() {
    fail("失敗するテスト");
}
// このメソッドの処理実行後にtearDown()を実行

// このメソッドの処理実行前にinit()を実行
@Test
@Disabled("デモ用")
void skippedTest() {
    // 実行されない
}
// このメソッドの処理実行後にtearDown()を実行

@AfterEach
void tearDown() {
}

@BeforeAll, @AfterAll

全体の初期化処理、全体終了後の後始末などに利用可能なアノテーション。
基本的にstaticにする。

@BeforeAll
static void initAll() {
}

// 全体として、テストメソッドの処理前にinitAll()を一度だけ実行

@Test
void succeedingTest() {
}

@Test
void failingTest() {
    fail("失敗するテスト");
}

@Test
@Disabled("デモ用")
void skippedTest() {
    // 実行されない
}

// 全体として、すべてのテストメソッドの処理後にtearDownAll()を一度だけ実行

@AfterAll
void tearDownAll() {
}

その他のアノテーション

@RepeatedTest ⇒ 繰り返しのテスト
@DisplayName ⇒ テスト結果のメソッドの表示名を編集
@Nested ⇒ テストメソッドを入れ子で管理
...

いろいろあるので、公式ドキュメントで随時確認。

JUnit5を動かすには

Spring + Maven環境だと少なくとも以下が必要

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <scope>test</scope>
</dependency>

mockitoと合わせて使うなら、
ExtendWithを忘れないこと、jupiterとついたものをインポートすることに注意。

SampleTest.java
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.test;
...

@ExtendWith(SpringExtension.class)
@ExtendWith(MockitoExtension.class)
class SampleTest {
  assertEquals(1, 1);
}

テストファイル配置場所

mavenディレクトリ構成で、
src/test/java/{ドメイン名}/ 配下で {実装クラス名}Test.java ファイルを置く。

例)
ドメイン名が example.com で、SampleApplication.javaのテストファイルならば、
src/test/java/com/example/SampleApplivationTest.java とする。

BDD Style(おまけ)

BDD - behavior-driven development(振舞駆動開発)を意識して、
ユーザストーリ、アプリの振る舞いに従った形式でテストを記述するのもあり。

参考資料

Discussion