unit-testing について
Java と Mockito でテストを書く開発者向けの単体テストガイドです。テスト構成や命名、スタブ、検証、確認項目を扱います。
主な機能
- 01独立した自己完結型テスト
- 02Arrange・Act・Assert の構成
- 03Mockito のスタブと検証の指針
- 04命名とテストデータの規約
活用シーン
- 01サービスメソッドの単体テスト作成
- 02テストクラスの Mockito 使用状況の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ folio-org/ folio-eureka-ai-dev --skill unit-testingインストール後、Claude Code で/unit-testingを入力して読み込まれたか確認します。
同梱ファイルreferences/examples.mdreferences/patterns.md
詳細
単体テストのガイドライン
一般原則
1. テストの独立性
- 各テストは独立しており、自己完結している必要があります
- テストは実行順序に依存してはいけません
- 可能な場合は、共有のインスタンス変数ではなく、テストメソッド内でローカル変数を使用してください
2. 明確なテスト名
次のパターンに従ってください:methodName_scenario_expectedBehavior
例:tableExists_positive_tableIsPresent、deleteTimer_negative_timerDescriptorIdIsNull
3. テストの構成
すべてのテストを Arrange → Act → Assert の順に構成してください
Mockito のベストプラクティス
1. Lenient モードは使用しない
❌ @MockitoSettings(strictness = Strictness.LENIENT) は追加しないでください。代わりに、正確なテストを記述してください。
2. 不要なスタブを避ける
テストで実際に使用するものだけをスタブ(when())してください。必要とするテストだけから呼び出されるヘルパーメソッドを使用してください:
private void setupContextMocks() {
when(context.getFolioModuleMetadata()).thenReturn(moduleMetadata);
when(context.getTenantId()).thenReturn(TENANT_ID);
when(moduleMetadata.getDBSchemaName(TENANT_ID)).thenReturn(SCHEMA_NAME);
}
3. 冗長な verify 文を削除する
when() でモック化していないインタラクションだけを検証してください:
verify(connection).close(); // ✅ not mocked
verify(dataSource).getConnection(); // ❌ redundant — already mocked
4. any() マッチャーより具体的なオブジェクトのスタブを優先する
テストデータを制御できる場合は、具体的なオブジェクトを使用してください:
when(mapper.convert(input)).thenReturn(output); // ✅
when(mapper.convert(any(InputType.class))).thenReturn(output); // ❌
any() は次の場合にのみ使用してください:サービスがオブジェクトを予測不能な形で変更する場合、複数の異なるオブジェクトが渡される可能性がある場合、または引数キャプターを使用する場合。
5. 基本的な戻り値にはシンプルな when().thenReturn() を使用する
when(service.process(data)).thenReturn(true); // ✅
doAnswer(inv -> true).when(service).process(data); // ❌
計算された戻り値、条件分岐、または副作用がある場合にのみ thenAnswer() を使用してください。
6. サービスの処理フロー全体をスタブする
テスト対象のコードパスにある、すべての外部呼び出しをスタブしてください:
when(repository.findByKey(key)).thenReturn(Optional.empty());
when(repository.save(entity)).thenReturn(entity);
when(repository.findById(id)).thenReturn(Optional.of(entity));
7. verifyNoMoreInteractions は慎重に使用する
すべてのインタラクションを網羅的に確認する必要があるモックだけを含めてください:
@AfterEach
void tearDown() {
verifyNoMoreInteractions(dataSource, connection, databaseMetaData, resultSet);
}
テストの構造
1. クラスのセットアップ
@UnitTest
@ExtendWith(MockitoExtension.class)
class MyServiceTest {
private static final String CONSTANT_VALUE = "test-value";
@Mock
private Dependency1 dependency1;
@AfterEach
void tearDown() {
verifyNoMoreInteractions(dependency1);
}
}
2. 個別のテスト
@Test
void methodName_scenario_expectedBehavior() throws Exception {
// Arrange
setupCommonMocks();
var service = new MyService(dependency1);
when(dependency1.doSomething()).thenReturn(expectedValue);
// Act
var result = service.methodUnderTest();
// Assert
assertThat(result).isEqualTo(expectedValue);
verify(dependency1).close();
}
検証パターン
1. 成功する操作
@Test
void operation_positive_success() throws Exception {
setupRequiredMocks();
when(repository.find()).thenReturn(entity);
var result = service.operation();
assertThat(result).isNotNull();
verify(connection).close();
}
2. 例外のシナリオ
@Test
void operation_negative_throwsException() throws Exception {
var expectedException = new SQLException("Connection failed");
when(dataSource.getConnection()).thenThrow(expectedException);
assertThatThrownBy(() -> service.operation())
.isInstanceOf(DataRetrievalFailureException.class)
.hasMessageContaining("Failed to perform operation")
.hasCause(expectedException);
}
一般的なパターン
1. テストデータ用の定数
複数のテストで再利用できるよう、クラスレベルで定義します:
private static final String MODULE_ID = "mod-foo-1.0.0";
private static final String TENANT_ID = "test-tenant";
2. ヘルパーメソッド
同じコードが 2 つ以上のテストに登場する場合は、オブジェクトの作成、アサーション、オブジェクトの検索、モックのセットアップ用に private static メソッドを抽出してください。
パラメーター化テスト、ディープコピーのスタブ、フルーエント API、厳密なマッチング、ラムダの明示的な型などの高度なパターンについては、references/patterns.md を参照してください。
テストクラスの完全な例については、references/examples.md を参照してください。
チェックリスト
-
@MockitoSettings(strictness = Strictness.LENIENT)を使用していない - 不要なスタブがない — すべての
when()文が使用されている - モック化していないメソッドだけを検証している
- テストデータを制御できる場合、
any()より具体的なオブジェクトのスタブを優先している - 基本的な戻り値にはシンプルな
when().thenReturn()を使用している - コードパス内のすべてのリポジトリ/サービスとのインタラクションをスタブしている
- サービスが内部コピーを作成する場合、コピー操作をスタブしている
- テスト名が
methodName_scenario_expectedBehaviorのパターンに従っている - 各テストが独立している
- リソース(接続、ストリーム)が閉じられることを検証している
- 例外テストで型、メッセージ、原因を検証している
- 再利用可能なテストデータに定数を使用している
- ヘルパーメソッドでコードの重複を解消している
- Arrange-Act-Assert のセクションが明確である
- テストデータの作成にフルーエント API を使用している
- パラメーターが 1 つのパラメーター化テストでは
Stream<Type>を使用している - var による推論が失敗するラムダには、型を明示的に宣言している
- 厳密なマッチングのテストに、マッチしない類似の値を含めている
- ユーティリティメソッドのすべてのエラーパスをテストしている
- 未使用のインポートを削除している