🧪
Java Spring Boot テストコードの書き方大全
はじめての人でも「何を」「どう書けば」動くテストになるかを、JUnit 5 + Mockito + Spring Boot 3 を前提にゼロから解説します。この記事だけで Web アプリの単体テスト〜統合テストまで一通り書けるようになることを目指します。
目次
- テストを書く理由
- 環境構築
- JUnit 5 超入門
- Mockito 基礎
- Spring Boot テストスライス完全攻略
- MockMvc で HTTP テスト
- Service 層のテスト
- Repository 層のテスト
- 外部依存の切り離し
- よくある落とし穴
- テスト設計 Tips
- まとめ
テストを書く理由
"動くコード" ではなく "壊れないコード" を目指すために、テストコードは必須です。
- リグレッションを防ぐ:仕様追加でどこかが壊れても、テストが落ちればすぐ気付く
- 安全なリファクタ:緑のランプがあれば大胆に書き換えられる
- 設計の補助線:テストしづらいコードは密結合。テストを書くと自然に疎結合設計へ
環境構築
Gradle 依存(抜粋)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test' // JUnit + Mockito 等
testImplementation 'org.mockito:mockito-inline:5.2.0' // static モック用
testImplementation 'org.testcontainers:junit-jupiter:1.19.3' // Testcontainers
}
Spring Initializr で雛形を作る
curl https://start.spring.io/starter.zip \
-d dependencies=web \
-d javaVersion=17 \
-o demo.zip && unzip demo.zip
JUnit 5 超入門
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test // これは 1 つのテストケース
void add_returnsSum() {
Calculator calc = new Calculator();
int actual = calc.add(2, 3);
assertEquals(5, actual); // 期待と実際を比較
}
}
- @Test を付けたメソッドが実行対象
- アサーションは
Assertions.*を静的 import すると読みやすい
前後処理
@BeforeEach void setUp() { /* 毎回呼ばれる */ }
@AfterEach void tearDown() { /* 片付け */ }
Mockito 基礎
モック生成とスタブ
UserRepository repo = mock(UserRepository.class);
when(repo.findById(1L)).thenReturn(Optional.of(new User("Alice")));
-
mock()でダミーオブジェクトを作る -
when(...).thenReturn(...)で戻り値を固定
void メソッド
doNothing().when(repo).deleteById(anyLong()); // 何もせず通す
doThrow(new RuntimeException()).when(repo).deleteById(1L); // 例外を投げる
呼び出し検証
verify(repo).findById(1L); // 1 回呼ばれたか
verify(repo, times(2)).save(any()); // 2 回呼ばれたか
Spring Boot テストスライス完全攻略
| アノテーション | 読み込み範囲 | 用途 |
|---|---|---|
@SpringBootTest |
アプリ全体(Web サーバ起動) | E2E 〜統合テスト |
@WebMvcTest(Controller) |
Controller + Spring MVC 周辺 | Web 層の単体テスト |
@DataJpaTest |
Repository + JPA + DB 接続 | DAO / JPA テスト |
@JsonTest |
Jackson のシリアライズ/デシリアライズ | JSON の変換テスト |
ポイントは「必要最低限だけ コンテキストを読み込む」ことでテストが速く安定すること。
MockMvc で HTTP テスト
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService;
@Test
void createUser_returnsCreated() throws Exception {
when(userService.create(any())).thenReturn(1L);
mockMvc.perform(post("/users")
.content("{\"name\":\"Bob\"}")
.contentType("application/json"))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/users/1"));
}
}
手順:
-
@WebMvcTestで Controller だけ読み込む - 依存サービスは
@MockBeanで差し替える -
post("/path")などでリクエストを組み立て、andExpectでレスポンスを検証
Service 層のテスト
class BillingServiceTest {
PaymentGateway gateway = mock(PaymentGateway.class);
BillingService service = new BillingService(gateway);
@Test
void pay_success() {
when(gateway.charge(anyInt())).thenReturn(true);
boolean result = service.pay(1000);
assertTrue(result);
verify(gateway).charge(1000);
}
}
依存を Mockito でモック → ビジネスロジックのみ検証。
Repository 層のテスト
@DataJpaTest
class UserRepositoryTest {
@Autowired UserRepository repo;
@Test
void save_and_find() {
User saved = repo.save(new User("Carol"));
Optional<User> found = repo.findById(saved.getId());
assertTrue(found.isPresent());
assertEquals("Carol", found.get().getName());
}
}
組み込み H2 DB が自動起動。@DataJpaTest だけで高速。
外部依存の切り離し
static メソッドをモック
try (MockedStatic<AuthUtil> mock = mockStatic(AuthUtil.class)) {
mock.when(() -> AuthUtil.verify(anyString())).thenAnswer(i -> null);
// テスト本体
}
Testcontainers
@Testcontainers
class PostgresContainerTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@DynamicPropertySource
static void overrideProps(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", postgres::getJdbcUrl);
r.add("spring.datasource.username", postgres::getUsername);
r.add("spring.datasource.password", postgres::getPassword);
}
}
本物の DB を Docker で起動し、統合テストでも環境差異ゼロ。
よくある落とし穴
-
ContentType.notSet:.contentType()を忘れている -
nullPointerException:@MockBean を付け忘れ、実体 Bean が起動してない - 静的メソッドがモックできない → mockito-inline 追加を忘れている
テスト設計 Tips
- メソッド名は GIVEN_WHEN_THEN で可読性アップ
- Faker / Instancio でランダムなテストデータ生成
- 1 テスト 1 アサーションを意識 ― 落ちた箇所が一目でわかる
- CI では
./gradlew testを必ずパイプラインに載せる
まとめ
- 小さい単位(Service・Repository)からテストを書こう
- Controller は MockMvc + @WebMvcTest が定番
- 外部依存をモックすればテストは速く壊れにくい
- Mockito と JUnit だけ で 8〜9割のケースは賄える
- どうしても static なら mockito-inline、本物のミドルウェアを試すなら Testcontainers
この記事をベースに、自分のプロジェクト構成に合わせてスライスやモックの粒度を調整すれば、実案件でも十分通用するテストスイートが組めます。
「テストはコスト」ではなく「安定稼働への投資」。小さなテストを積み上げて、バグのない Spring Boot アプリを育てていきましょう。
Discussion