Gradle 依存関係の設定
JUnit 5 と Mockito を使うための最小限の build.gradle 設定を示します。Spring Boot プロジェクトでは spring-boot-starter-test が両者を一括で含んでいます。
// build.gradle (Gradle 8.x)
plugins {
id 'java'
}
dependencies {
// JUnit 5 (Jupiter)
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
// Mockito
testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'
// AssertJ (推奨アサーションライブラリ)
testImplementation 'org.assertj:assertj-core:3.25.3'
}
test {
useJUnitPlatform() // JUnit 5 を有効化する必須設定
}
ℹ️ Spring Boot の場合
testImplementation 'org.springframework.boot:spring-boot-starter-test' を追加するだけで JUnit 5・Mockito・AssertJ がすべて含まれます。
JUnit 5 主要アノテーション
JUnit 4 からの移行で最初に覚えるべきアノテーションをまとめます。
| アノテーション | 役割 | JUnit 4 相当 |
|---|---|---|
@Test | テストメソッドを宣言 | @Test |
@BeforeEach | 各テストの前に実行 | @Before |
@AfterEach | 各テストの後に実行 | @After |
@BeforeAll | クラス内で一度だけ実行(static 必須) | @BeforeClass |
@AfterAll | クラス内で一度だけ実行(static 必須) | @AfterClass |
@DisplayName | テスト名を日本語で表示 | なし |
@Disabled | テストを一時的にスキップ | @Ignore |
@Tag | テストをグループ化(CI フィルタに使用) | @Category |
基本的なテストクラスのサンプルです。
import org.junit.jupiter.api.*;
import static org.assertj.core.api.Assertions.*;
@DisplayName("CalculatorService のテスト")
class CalculatorServiceTest {
CalculatorService sut; // System Under Test
@BeforeEach
void setUp() {
sut = new CalculatorService(); // 毎テスト前に初期化
}
@Test
@DisplayName("正の整数同士の加算は正しい結果を返す")
void add_positiveNumbers_returnsSum() {
int result = sut.add(3, 5);
assertThat(result).isEqualTo(8);
}
@Test
@DisplayName("ゼロ除算は ArithmeticException をスロー")
void divide_byZero_throwsException() {
assertThatThrownBy(() -> sut.divide(10, 0))
.isInstanceOf(ArithmeticException.class)
.hasMessageContaining("zero");
}
}
パラメータ化テスト
同じテストロジックを複数の入力値で実行する @ParameterizedTest は、境界値・同値分割を網羅する際に非常に効果的です。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.assertj.core.api.Assertions.*;
class PasswordValidatorTest {
PasswordValidator validator = new PasswordValidator();
// ─────────────────────────────────────────
// @CsvSource : 複数の入力/期待値ペアを記述
// ─────────────────────────────────────────
@ParameterizedTest(name = "パスワード="{0}" → {1}")
@CsvSource({
"Abc1234!, true", // 正常:8文字以上・大文字・数字・記号
"abc1234!, false", // NG:大文字なし
"Abcdefg!, false", // NG:数字なし
"Abc1234, false", // NG:記号なし
"Abc1!, false" // NG:7文字(最小8文字未満)
})
void validate_passwords(String password, boolean expected) {
assertThat(validator.isValid(password)).isEqualTo(expected);
}
// ─────────────────────────────────────────
// @MethodSource : 複雑なオブジェクトを渡す場合
// ─────────────────────────────────────────
@ParameterizedTest
@MethodSource("provideUserAgeAndCategory")
void categorize_user(int age, String expectedCategory) {
assertThat(validator.categorize(age)).isEqualTo(expectedCategory);
}
static java.util.stream.Stream<org.junit.jupiter.params.provider.Arguments>
provideUserAgeAndCategory() {
return java.util.stream.Stream.of(
Arguments.of(17, "MINOR"),
Arguments.of(18, "ADULT"),
Arguments.of(64, "ADULT"),
Arguments.of(65, "SENIOR")
);
}
}
@Nested でテストを構造化する
@Nested を使うとテストクラスを階層化でき、条件ごとにグループを明確に分けられます。IDEのテスト結果ツリーが読みやすくなる利点もあります。
@DisplayName("OrderService")
class OrderServiceTest {
OrderService sut;
@BeforeEach
void setUp() { sut = new OrderService(); }
@Nested
@DisplayName("注文作成")
class CreateOrder {
@Test
@DisplayName("在庫がある場合は注文を作成できる")
void whenInStock_createSucceeds() {
Order order = sut.create("ITEM-001", 1);
assertThat(order.getStatus()).isEqualTo(OrderStatus.CREATED);
}
@Test
@DisplayName("在庫不足の場合は OutOfStockException をスロー")
void whenOutOfStock_throwsException() {
assertThatThrownBy(() -> sut.create("ITEM-999", 100))
.isInstanceOf(OutOfStockException.class);
}
}
@Nested
@DisplayName("注文キャンセル")
class CancelOrder {
@Test
@DisplayName("CREATED 状態の注文はキャンセル可能")
void whenCreated_cancelSucceeds() {
Order order = sut.create("ITEM-001", 1);
sut.cancel(order.getId());
assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED);
}
}
}
Mockito 基礎 — モックとスタブ
Mockito では外部依存(リポジトリ・外部API・メール送信など)をモックに置き換え、テスト対象のロジックだけを検証します。
ℹ️ @ExtendWith(MockitoExtension.class) を忘れずに
JUnit 5 で Mockito のアノテーション(@Mock・@InjectMocks)を使うには、クラスに @ExtendWith(MockitoExtension.class) が必要です。
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("UserService のテスト")
class UserServiceTest {
@Mock
UserRepository userRepository; // モック化
@Mock
EmailService emailService; // モック化
@InjectMocks
UserService sut; // モックを自動注入
@Test
@DisplayName("登録済みユーザーIDでユーザー名を取得できる")
void findName_existingUser_returnsName() {
// ─ Arrange(スタブ定義)─
User mockUser = new User("U001", "山田太郎");
when(userRepository.findById("U001")).thenReturn(Optional.of(mockUser));
// ─ Act ─
String name = sut.findName("U001");
// ─ Assert ─
assertThat(name).isEqualTo("山田太郎");
}
@Test
@DisplayName("存在しないユーザーIDは UserNotFoundException をスロー")
void findName_nonExistingUser_throwsException() {
when(userRepository.findById("X999")).thenReturn(Optional.empty());
assertThatThrownBy(() -> sut.findName("X999"))
.isInstanceOf(UserNotFoundException.class)
.hasMessage("User not found: X999");
}
@Test
@DisplayName("ユーザー登録時にウェルカムメールが送信される")
void register_sendsWelcomeEmail() {
User newUser = new User("U002", "鈴木花子");
when(userRepository.save(any(User.class))).thenReturn(newUser);
sut.register("鈴木花子", "hanako@example.com");
// emailService.send() が1回呼ばれたことを検証
verify(emailService, times(1)).send(eq("hanako@example.com"), anyString());
}
}
verify と ArgumentCaptor
verify でメソッドの呼び出し回数と引数を検証します。引数の詳細な内容を確認したい場合は ArgumentCaptor を使います。
@Test
@DisplayName("パスワードリセット時にトークンをメールで送信する")
void resetPassword_sendsTokenEmail() {
// ─ Arrange ─
User user = new User("U001", "山田太郎");
user.setEmail("taro@example.com");
when(userRepository.findById("U001")).thenReturn(Optional.of(user));
// ─ Act ─
sut.resetPassword("U001");
// ─ ArgumentCaptor でメール引数を捕捉 ─
ArgumentCaptor<String> toCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> bodyCaptor = ArgumentCaptor.forClass(String.class);
verify(emailService).send(toCaptor.capture(), bodyCaptor.capture());
// 送信先が正しいか
assertThat(toCaptor.getValue()).isEqualTo("taro@example.com");
// メール本文にトークンが含まれているか
assertThat(bodyCaptor.getValue()).contains("reset-token");
}
@Test
@DisplayName("ユーザーが存在しない場合はメールを送らない")
void resetPassword_nonExistingUser_noEmailSent() {
when(userRepository.findById("X999")).thenReturn(Optional.empty());
assertThatThrownBy(() -> sut.resetPassword("X999"))
.isInstanceOf(UserNotFoundException.class);
// emailService は一切呼ばれないことを検証
verifyNoInteractions(emailService);
}
スパイ(@Spy)の活用
スパイは実クラスのインスタンスをラップし、特定のメソッドだけをスタブに差し替えます。完全なモックが難しいクラス(レガシーコードなど)のテストに有効です。
@ExtendWith(MockitoExtension.class)
class NotificationServiceTest {
// 実クラスを使いつつ一部だけスタブ化
@Spy
NotificationService sut = new NotificationService();
@Test
@DisplayName("スパイで外部送信だけをスタブ化")
void notify_callsInternalLogic() {
// send() だけスタブ(実際の HTTP 送信を回避)
doReturn(true).when(sut).sendExternalApi(anyString(), anyString());
boolean result = sut.notify("USER-01", "テスト通知");
assertThat(result).isTrue();
// 内部のログ記録ロジックは実際に実行される
verify(sut).logNotification("USER-01", "テスト通知");
}
}
// ─────────────────────────────────────
// doReturn vs thenReturn の使い分け
// ─────────────────────────────────────
// スパイで実メソッドの呼び出しを避けたい場合は doReturn() を使う
// doReturn(value).when(spy).method(); // メソッドを呼ばずにスタブ
// when(spy.method()).thenReturn(value); // メソッドが一度実行される(副作用に注意)
実務でのベストプラクティス
⚠️ モックしすぎに注意
すべての依存をモックにすると「実装の詳細を検証するテスト」になりがちです。テストは「仕様(振る舞い)」を検証するものです。モックは外部 I/O(DB・API・メール)に留め、内部ロジックは実オブジェクトで検証するのが基本です。
⚠️ Mockito の静的メソッドモック
mockStatic() は使いやすいですが、静的メソッドへの依存はテスタビリティを下げます。静的メソッドを使う実装はインターフェースでラップして DI 注入できる設計に改善することを検討してください。
✅ テスト名は「状態 → 操作 → 結果」パターンで
メソッド名は methodName_condition_expectedResult() の形式(例:findName_existingUser_returnsName)にするとテスト失敗時に何が壊れたか即座にわかります。
まとめ
本記事で解説したポイントを整理します。
- @BeforeEach / @AfterEach でセットアップ・クリーンアップを確実に行い、テスト間の独立性を保つ
- @ParameterizedTest + @CsvSource / @MethodSource で境界値・同値クラスを網羅的にテストする
- @Nested でテストを「条件ごとのグループ」に整理して可読性を上げる
- @Mock + @InjectMocks で外部 I/O を排除し、テスト対象ロジックを孤立検証する
- ArgumentCaptor で「何が渡されたか」まで検証し、振る舞いをきめ細かく確認する
- @Spy はレガシーコードのテストや一部だけスタブ化したい場合に使い、乱用は避ける