🧪

Java Spring Boot テストコードの書き方大全

に公開

はじめての人でも「何を」「どう書けば」動くテストになるかを、JUnit 5 + Mockito + Spring Boot 3 を前提にゼロから解説します。この記事だけで Web アプリの単体テスト〜統合テストまで一通り書けるようになることを目指します。

目次

  1. テストを書く理由
  2. 環境構築
  3. JUnit 5 超入門
  4. Mockito 基礎
  5. Spring Boot テストスライス完全攻略
  6. MockMvc で HTTP テスト
  7. Service 層のテスト
  8. Repository 層のテスト
  9. 外部依存の切り離し
  10. よくある落とし穴
  11. テスト設計 Tips
  12. まとめ

テストを書く理由

"動くコード" ではなく "壊れないコード" を目指すために、テストコードは必須です。

  • リグレッションを防ぐ:仕様追加でどこかが壊れても、テストが落ちればすぐ気付く
  • 安全なリファクタ:緑のランプがあれば大胆に書き換えられる
  • 設計の補助線:テストしづらいコードは密結合。テストを書くと自然に疎結合設計へ

環境構築

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"));
    }
}

手順:

  1. @WebMvcTest で Controller だけ読み込む
  2. 依存サービスは @MockBean で差し替える
  3. 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 を必ずパイプラインに載せる

まとめ

  1. 小さい単位(Service・Repository)からテストを書こう
  2. Controller は MockMvc + @WebMvcTest が定番
  3. 外部依存をモックすればテストは速く壊れにくい
  4. Mockito と JUnit だけ で 8〜9割のケースは賄える
  5. どうしても static なら mockito-inline、本物のミドルウェアを試すなら Testcontainers

この記事をベースに、自分のプロジェクト構成に合わせてスライスやモックの粒度を調整すれば、実案件でも十分通用するテストスイートが組めます。

「テストはコスト」ではなく「安定稼働への投資」。小さなテストを積み上げて、バグのない Spring Boot アプリを育てていきましょう。

Discussion