前置き
メリット・デメリットに続いて、実装したソースを紹介します。
ディレクトリ構成や、アーキテクチャに関しては過去記事【Spring Boot】個人的に推奨するディレクトリ/ファイル構成を参照お願いします。
ソース全貌
テスト方針
テスト対象・粒度
ざっくりテストを行う際の概要をまとめました。
今回の自動テストでは単体テストが対象になります。
| テスト種別 | 目的・概要 | 粒度 | 使用技術・ツール例 | 主な確認内容 |
|---|---|---|---|---|
| 単体テスト | 各クラス・メソッドが単独で正しく動作するかを検証 | Controller、Service、Repository階層毎に実施 | JUnit/Mockito | ロジック検証、例外処理、境界値、依存のモック化 |
| 結合テスト | 各クラス・メソッド間が正しく連携して動作するか | 階層をまたいで実施 | SpringBootTest/MockMvc | APIレスポンス、メソッド間での連携 |
| E2E/システムテスト | システム全体が期待通り動作するか | アプリ全体 | Selenium/Postman等 | API連携、業務フロー全体、外部サービス連携 |
| 非機能テスト | 性能・セキュリティなど品質特性の検証 | アプリを超えてサーバー含む全体(性能、セキュリティなど) | JMeter//SonarQube等 | 負荷、脆弱性 |
| リグレッションテスト | 修正後も既存機能が壊れていないか確認 | 追加・変更部分+変更部分に依存している機能 | CI(GitHubActions等) | 過去のバグ再発防止、自動テストの定期実行 |
テスト実装方針
テスト対象はController、Service層とし、Controller層のテストを行う際はService層をモック化するといった方針で実装しています。
Repository層のテストは今回の記事では対象外としています。
実装
準備
- テストに必要なパッケージをimportするため今回はbuild.gradleのdependenciesに以下を追記しました。
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.5'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
- テストコードは各プロジェクトのsrc/testフォルダ配下にmainフォルダと同様のフォルダ構成で配置します。
テスト実装
- Mockito用のアノテーション付与
@ExtendWithを付与しないとMockitoが効かないので重要です。
/**
* UsersControllerのテストクラス.
*/
@ExtendWith(MockitoExtension.class)
public class UsersControllerTest {
- テスト対象クラスへのモック注入
@InjectMocksを付与したクラスに@Mockを付与したクラスが注入されます。
この段階ではUsersServiceクラス内のメソッドはモック化されているので、返却値がnullだったり何も動作しないメソッドになってます。
/**
* モック注入対象.
*/
@InjectMocks
private UsersController controller;
/**
* UsersServiceモック化.
*/
@Mock
private UsersService service;
- モック化したクラスの動作実装
UsersServiceクラスのgetUsersメソッドの動作をuserId("001")を受け取ったらuserId("001").lastName("RPC").firstName("太郎").age(20)を返却するよう実装しています。
// モック設定.
var getUsersRequestModel = GetUsersRequestModel.builder().userId("001").build();
var getUsersResponseModel = GetUsersResponseModel.builder().userId("001").lastName("RPC")
.firstName("太郎").age(20).build();
when(service.getUsers(getUsersRequestModel)).thenReturn(getUsersResponseModel);
- テスト実施 - メソッドの検証
テストしたいControllerのメソッドを実行して返却値を検証します。
検証にはJunit5のアサーションメソッドassertEqualsを使用します。
// 検証.
var expected = GetUsersResponse.builder().userId("001").lastName("RPC")
.firstName("太郎").age(20).build();
assertEquals(expected, actual);
- テスト実施 - バリデーション
@Sizeなど付与したクラスを検証するにはValidatorFactoryを利用して検証します。
エラーの数や、エラーメッセージの内容の検証はメソッドの検証同様にJunit5のアサーションメソッドを使用します。
サンプルコードではmessage.propertiesを読み込むためTestUtilクラスに共通メソッドを用意してValidatorFactoryを生成しています。
@Test
void バリデーション_userIdが空文字() {
// リクエスト.
var getUsersRequest = GetUsersRequest.builder().userId("").build();
// 検証.
try (ValidatorFactory factory = testUtil.getValidatorFactory()) {
var validator = factory.getValidator();
Set<ConstraintViolation<GetUsersRequest>> violations = validator.validate(getUsersRequest);
// 検証.
assertEquals(1, violations.size());
assertThat(violations).extracting("message")
.containsOnly("空白は許可されていません");
}
}
- テスト実施 - 例外処理
例外処理を実装したクラスはassertThrowsを使用して検証します。
ageが負の値だと例外をThrowするように実装しているので以下のように、assertThrowsで正しい例外か検証し、assertEqualsでエラーメッセージの内容を検証します。
@Test
void 処理が異常終了する() {
// リクエストモデル作成.
var postUsersRequest = PostUsersRequest.builder().lastName("RPC")
.firstName("太郎").age(-1).build();
// 検証.
var exception = assertThrows(NumberFormatException.class,
() -> controller.postUsers(postUsersRequest));
assertEquals("年齢は正の値のみ", exception.getMessage());
}
Service層のテストについてはモック化する対象がRepository層になるだけで、実装内容はController層と同じなので割愛します。
基本的なアサーションまとめ
よく使う系
| メソッド | 概要 | 使用例 |
|---|---|---|
assertEquals(expected, actual) |
期待値と実際の値が等しいか | assertEquals(5, result); |
assertNotEquals(unexpected, actual) |
等しくないことを確認 | assertNotEquals(0, result); |
assertTrue(condition) |
条件が true
|
assertTrue(list.isEmpty()); |
assertFalse(condition) |
条件が false
|
assertFalse(flag); |
assertNull(object) |
null である |
assertNull(value); |
assertNotNull(object) |
null でない |
assertNotNull(user); |
文字列・オブジェクト比較
| メソッド | 概要 | 使用例 |
|---|---|---|
assertSame(expected, actual) |
同一インスタンスか | assertSame(obj1, obj2); |
assertNotSame(unexpected, actual) |
異なるインスタンス | assertNotSame(obj1, obj2); |
assertEquals(expected, actual, delta) |
浮動小数点を誤差範囲内で比較 | assertEquals(1.0, result, 0.01); |
配列・コレクション
| メソッド | 概要 | 使用例 |
|---|---|---|
assertArrayEquals(expected, actual) |
配列が等しい | assertArrayEquals(exp, act); |
assertIterableEquals(expected, actual) |
Iterableの比較 | assertIterableEquals(list1, list2); |
assertLinesMatch(expected, actual) |
行単位比較(正規表現可) | assertLinesMatch(exp, act); |
例外系アサーション
| メソッド | 概要 | 使用例 |
|---|---|---|
assertThrows(Exception.class, executable) |
例外が投げられる | assertThrows(IllegalArgumentException.class, () -> method()); |
assertDoesNotThrow(executable) |
例外が出ない(voidメソッド等でよく使う) | assertDoesNotThrow(() -> method()); |
複数アサーション
| メソッド | 概要 | 使用例 |
|---|---|---|
assertAll(...) |
複数検証をまとめて実行 | assertAll(() -> assertEquals(1,a), () -> assertNotNull(b)); |
タイムアウト
| メソッド | 概要 | 使用例 |
|---|---|---|
assertTimeout(duration, executable) |
指定時間内に完了 | assertTimeout(Duration.ofMillis(100), () -> method()); |
assertTimeoutPreemptively(duration, executable) |
強制タイムアウト | assertTimeoutPreemptively(Duration.ofSeconds(1), () -> method()); |
メッセージ付きアサーション
| 書き方 | 概要 |
|---|---|
assertEquals(a, b, "message") |
デバッグする際に便利 |
まとめ
本記事では、Spring Boot における 単体テストの実装例 を中心に、
- テスト種別と今回のテスト範囲(単体テスト)
- Controller / Service 層のテスト方針
- Mockito を用いた依存クラスのモック化
- JUnit5 の基本的なアサーションの使い方
について紹介しました。
特に、Controller 層のテストでは
「Service 層をモック化し、Controller の責務に集中して検証する」
という方針を取ることで、テストの目的が明確になり、保守性の高いテストコードになります。
また、バリデーションや例外処理についても自動化することで今後発生するであろう改修の際のテスト工数を削減できるようになります。
以上@SpringBootTestを使わない自動テストの紹介でした。
この記事が、誰かの参考になれば幸いです。
