【Java】JUnit 5によるテストの基本
導入
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
に以下を追加します。
<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
に以下を追加します。
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
}
test {
useJUnitPlatform()
}
repositories
は、Maven のセントラルリポジトリを参照するようにmavenCentral()
を指定します。
JCenter()
となっている場合は、修正してください。
テストの実行
まずは簡単なテストを作成してみます。
引数の値を基に「Hello XXX!!」という文字列を返すメソッド(greet()
)を作成しました。
public class Hello {
public String greet(String name) {
return String.format("Hello %s!!", name);
}
}
これが正しく動作するかを以下のテストコードで検証します。
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()
で値が等しいと判断された場合、失敗は値が異なった場合、エラーは例外が発生した場合を意味します。
- 成功の例
- 失敗の例
テストパターンがしっかり作れており、すべてのステータスが成功であれば、ある程度の品質が担保されるということになります。 逆に失敗やエラーがある場合は、バグや仕様が違うことを疑います。
検証方法の種類
上記では値の検証にassertEquals()
を使用しましたが、他に以下のようなものがあります。
比較する値(引数)の型ごとにメソッドが用意されていますが、すべてを記載するのは大変なため簡略化しています。
詳細はこちらを確認してください。
※以下は JUnit 5.8.1 を対象としています。バージョンの違いにより使用できないものがあります。
メソッド | 説明 |
---|---|
assertEquals(x, y) | x とy が等しい(equals() == true である)ことを検証 |
assertNotEquals(x, y) | x とy が等しくない(equals() == false である)ことを検証 |
assertArrayEquals(x, y) | 配列x とy のすべての要素が順番通りに等しい(equals() == true である)ことを検証 |
assertIterableEquals(x, y) | Iteratable (List など)なx とy のすべての要素が順番通りに等しい(equals() == true である)ことを検証 |
assertNotNull(x) | x がnull でないことを検証 |
assertTrue(x) | x がtrue であることを検証 |
assertFalse(x) | x がfalse であることを検証 |
assertSame(x, y) | x とy が同一オブジェクト(x == y である)ことを検証 |
assertNotSame(x, y) | x とy が異なるオブジェクト(x != y である)ことを検証 |
assertInstanceOf(c, x) | x が指定したクラスc のオブジェクトであることを検証 |
assertLinesMatch(x, y) | y がx で指定したパターンの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));
各要素は、String
のmatches()
メソッドによって判定され、
すべての要素が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
を戻り値にする必要があります。
複数という指定は、上記のように配列を用いる他に、Collection
、Iterable
、Iterator
、Stream
が使用できます。
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() {
//...
}