概要
テストコードは書けるけれど「何をどこまでテストすべきか」で毎回迷う、JUnit 4 から 5 への移行で何が変わったのか整理できていない――そんな声は現場で少なくありません。JUnit 5 は Jupiter エンジンの導入により、アノテーション体系が刷新され、@DisplayName による日本語テスト名、@Nested によるグループ化、@ParameterizedTest による表形式テストなど、実務で使いやすい機能が揃いました。この記事では、Calculator クラスを題材に、基本アノテーションの使い方、例外テスト(assertThrows)、複数アサーションの一括検証(assertAll)、パラメータ化テストまでを一通り整理します。テスト設計のコツとして Given-When-Then パターンにも触れ、読みやすく保守しやすいテストコードを書くための実践的な指針を示します。
使いどころ
新規開発のビジネスロジック(消費税計算、割合計算など)に対し、JUnit 5 のパラメータ化テストで境界値を網羅する
既存の JUnit 4 テストを JUnit 5 に移行し、@Nested と @DisplayName でテストの構造と可読性を改善する
例外が発生すべきケース(0除算、null 入力、範囲外の値)を assertThrows で明示的に検証し、回帰テストとして保護する
複数条件をまとめて確認したいサービスクラスで assertAll を使い、失敗を一度に洗い出す
レビューしやすいテスト命名に揃えるため、@DisplayName で業務用語ベースの表示名を整備する
コード例
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
// テスト対象: record で不変な Calculator を定義
record Calculator() {
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("0除算はできません");
}
return a / b;
}
}
private Calculator calc;
@BeforeEach
void setUp() {
calc = new Calculator();
}
@Nested
@DisplayName("加算テスト")
class AddTests {
@Test
@DisplayName("正の数の加算")
void addPositive() {
assertEquals(8, calc.add(3, 5));
}
@Test
@DisplayName("負の数を含む加算")
void addNegative() {
assertEquals(-2, calc.add(-5, 3));
}
@ParameterizedTest
@CsvSource({ "1,2,3", "0,0,0", "-1,1,0", "100,200,300" })
@DisplayName("様々な値の加算テスト")
void addParameterized(int a, int b, int expected) {
assertEquals(expected, calc.add(a, b));
}
}
@Nested
@DisplayName("除算テスト")
class DivideTests {
@Test
@DisplayName("正常な除算")
void divideNormal() {
assertEquals(5, calc.divide(10, 2));
}
@Test
@DisplayName("ゼロ除算で ArithmeticException が発生する")
void divideByZero() {
assertThrows(ArithmeticException.class,
() -> calc.divide(10, 0));
}
}
@Test
@DisplayName("複数のアサーションをまとめて検証")
void multipleAssertions() {
assertAll("四則演算の検証",
() -> assertEquals(8, calc.add(3, 5)),
() -> assertEquals(-2, calc.subtract(3, 5)),
() -> assertEquals(15, calc.multiply(3, 5)),
() -> assertEquals(2, calc.divide(10, 5))
);
}
@AfterEach
void tearDown() {
calc = null;
}
}Version Coverage
record でテスト対象の Calculator を不変に定義できる。@Nested でテストをグループ化し、@DisplayName で日本語テスト名を付けると構造が明確になる。
// Java 17: record + @Nested でテストを構造化
record Calculator() {
int add(int a, int b) { return a + b; }
int divide(int a, int b) {
if (b == 0) throw new ArithmeticException("0除算");
return a / b;
}
}
@Nested @DisplayName("加算テスト")
class AddTests {
@ParameterizedTest
@CsvSource({"1,2,3", "0,0,0", "-1,1,0"})
void add(int a, int b, int expected) {
assertEquals(expected, calc.add(a, b));
}
}Library Comparison
注意点
@Test は org.junit.jupiter.api.Test をインポートすること。org.junit.Test(JUnit 4)をインポートすると実行されない
@BeforeEach は各テストメソッドの前に毎回実行される。テスト間で状態を共有したい場合は @BeforeAll(static メソッド)を使うが、テストの独立性を損なうため慎重に使う
@ParameterizedTest と @Test を同じメソッドに付けると二重実行される。パラメータ化テストには @ParameterizedTest だけを付ける
assertAll に渡すラムダが1つでも失敗すると、残りも実行されたうえで全失敗が報告される。assertEquals を個別に書く場合は最初の失敗で中断されるため、挙動が異なる
@Nested クラスは内部クラス(non-static inner class)でなければならない。static をつけるとテストが認識されない
例外テストでは例外型だけでなくメッセージや原因も必要に応じて確認する。型だけだと別原因の失敗を見逃しやすい
1メソッドで複数の振る舞いを検証しすぎると失敗理由が読み取りにくい。正常系と異常系は分けて書く方が保守しやすい
FAQ
import を org.junit.jupiter.api に変更し、@Before を @BeforeEach に、@Rule を @ExtendWith に置き換えます。junit-vintage-engine を入れれば JUnit 4 テストも並行実行できます。
@ValueSource(単一値)、@EnumSource(enum 全値)、@MethodSource(メソッド参照)が実務でよく使われます。複雑なデータには @MethodSource が柔軟です。
技術的には問題ありませんが、@DisplayName に日本語を書き、メソッド名は英語にするのがチームでの運用上は無難です。CI のログでも文字化けしにくくなります。
同じ入力に対する複数の戻り値や DTO の各フィールドをまとめて確認したい場面で有効です。失敗を一度に確認できる反面、 unrelated な検証を1つに詰め込みすぎないようにします。
必須ではありません。テスト対象の状態や操作ごとにまとまりがあり、クラス分割すると読みやすくなるときに使うのが自然です。小さなテストクラスでは無理に導入しなくて構いません。