JUnit5テスト設計:タグ・フィクスチャー・パラレル実行まで徹底ガイド
1. カテゴリ化(@Tag)と構造化(@Nested)
目的:重いテストと軽いテストを分けて、ビルドやCIでの実行制御を可能にする。
1-1. @Tag の使い方
@Tag("integration")
@Test
void integrationTest() { … }
Gradle 設定例:
test {
useJUnitPlatform {
includeTags 'unit', 'fast'
excludeTags 'integration', 'slow'
}
}
CLI での切り替え:
./gradlew test -DincludeTags=unit ./gradlew test -DexcludeTags=integration
1-2. @Nested の使い方
@Nested
@Tag("validation")
class ValidationTests {
@Test void whenNameEmpty_thenError() { … }
}
ネストクラス個別実行:
./gradlew test --tests 'com.example.MyTestClass$ValidationTests*'
2. 共通フィクスチャー(セットアップ/後処理)
目的:重複コードを減らし、リソース初期化・解放を一元管理する。
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MyServiceTest {
private InMemoryDatabase db;
private ApiClient client;
@BeforeAll
void initAll() {
db = new InMemoryDatabase(); // 一度だけ初期化
client = mock(ApiClient.class);
}
@BeforeEach
void initEach() {
db.clear(); // 各テスト前にリセット
reset(client);
}
@AfterEach
void tearDownEach() {
db.rollback(); // 副作用を排除
}
@AfterAll
void tearDownAll() {
db.close(); // リソース解放
}
}
3. スローテスト問題と多面的解決策
問題の詳細:
- 外部リソース呼び出し(DB/HTTP)
- 大量データ準備
- 複雑な初期化処理(コンテナ起動など)
解決策:
- タグ分類(@Tag)で除外/含めるグループを制御
- モック/スタブ化で外部依存を置き換え
ApiClient api = mock(ApiClient.class); when(api.call(...)).thenReturn(mockData); - インメモリDB(H2)で本番DB接続を排除
dependencies { testImplementation 'com.h2database:h2:2.1.214' } - 並列実行でテスト時間を短縮
test { useJUnitPlatform { systemProperty 'junit.jupiter.execution.parallel.enabled','true' systemProperty 'junit.jupiter.execution.parallel.mode.default','concurrent' } } - 共通フィクスチャー最適化:重い処理は @BeforeAll、一部を @BeforeEach
- テストのリファクタリング:不要・重複テストの削除、データ量削減
4. 事前条件(Precondition)の記述
目的:特定環境のみ実行したいテストをスキップにする。
@Test
void onCiOnly() {
Assumptions.assumeTrue(
"true".equals(System.getenv("CI")),
"CI環境でないためスキップ"
);
// CI限定のテスト処理…
}
5. パラメータ化テスト
目的:同じロジックを複数の入力で一括検証し、重複を排除。
| アノテーション | 用途 |
|---|---|
| @ValueSource | 単一型のリテラル配列 |
| @CsvSource | 複数列の文字列ペア |
| @EnumSource | enum の全列挙値 |
| @MethodSource | メソッドで提供するストリーム |
@ParameterizedTest
@CsvSource({
"racecar, true",
"apple, false"
})
void isPalindrome(String input, boolean expected) {
assertEquals(expected, StringUtils.isPalindrome(input));
}
メリット:
1. テストケースの一覧性向上
2. 新規ケース追加が容易
3. 構造の一貫性維持
6. テストダブル(Dummy / Stub / Fake / Spy / Mock)
目的:外部依存を切り離し、テスト対象の振る舞い検証に集中する。
| 種類 | 説明 | Mockito 例 |
|---|---|---|
| Dummy | 値を渡すだけ | new DummyLogger() |
| Stub | 固定の戻り値を返す | when(client.get()).thenReturn(data); |
| Fake | 簡易実装(インメモリ) | new InMemoryUserRepo() |
| Spy | 実オブジェクトを監視 | spy(new ArrayList<>()) |
| Mock | 呼び出し検証 | verify(client, times(1)).get(); |
class UserServiceTest {
@Mock ApiClient client;
@InjectMocks UserService svc;
@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this);
when(client.fetchUser("u1"))
.thenReturn(new User("u1","Taro"));
}
@Test
void testGetUserName() {
assertEquals("Taro", svc.getUserName("u1"));
verify(client, times(1)).fetchUser("u1");
}
}
