Spring Bootのテストとは、アプリ全体または一部(Web層・DB層など)だけをテスト用に起動し、Controller・Service・Repositoryの動きを自動で確かめる仕組みです。 全体を起動するのが @SpringBootTest、Web層だけを起動するのが @WebMvcTest で、HTTPリクエストを模擬して送る道具が MockMvc(AssertJ形式なら MockMvcTester)です。

この記事では、どのアノテーションを選ぶかの使い分け表、Controller・Service・Repositoryそれぞれのコピペ用テスト一式、よく使うmatcher一覧、よくあるエラーと原因をまとめました。Spring Boot 4.1(公式ドキュメント 4.1.1)で確認したパッケージ名と依存(テスト用スターター)を明記しているので、@MockBean が見つからない、import が合わない、といった4系への移行時のつまずきも解消できます。JUnitやMockitoそのものの書き方は別記事に任せ、ここでは Spring Bootのテスト機能に絞って解説します。

この記事のコードは、検証環境でSpringのライブラリ(Maven Central)を取得できないため未実行です。公式リファレンス(Spring Boot 4.1.1)とAPIドキュメントに沿って記述しています。

Spring Bootテストの使い分け早見表

アノテーション 起動する範囲 速さ 主な用途 テスト用スターター(Boot 4)
@SpringBootTest アプリ全体 遅い 結合テスト、設定の確認 対象技術の spring-boot-starter-〇〇-test
@SpringBootTest + @AutoConfigureMockMvc 全体+MockMvc 遅い 全層を通したHTTPのテスト spring-boot-starter-webmvc-test
@WebMvcTest Controller・ControllerAdvice・Filter・JSON変換 速い Controllerのテスト spring-boot-starter-webmvc-test
@DataJpaTest JPA・Repository・組み込みDB 速い Repository・クエリのテスト spring-boot-starter-data-jpa-test
@JdbcTest / @DataJdbcTest JdbcTemplate / Spring Data JDBC 速い SQLのテスト spring-boot-starter-jdbc-test など
@JsonTest JSON変換(Jackson) とても速い JSONの形の確認 テスト用スターターに含まれる
@RestClientTest RestClient・MockRestServiceServer 速い 外部APIクライアントのテスト spring-boot-starter-restclient-test
@WebFluxTest WebFluxのController 速い リアクティブなController spring-boot-starter-webflux-test
アノテーションなし(JUnit+Mockito) Springを起動しない 最速 Serviceのロジック spring-boot-starter-test 相当

迷ったら「テストしたい層だけを起動するスライス(@WebMvcTest や @DataJpaTest)」を選び、全体を通す @SpringBootTest は要所だけにすると、テストが速く保てます。

テストしたいこと 選ぶもの
URL・ステータス・JSON・入力チェック @WebMvcTest + @MockitoBean
Serviceの計算・分岐 Springなし(@ExtendWith(MockitoExtension.class))
自作のクエリメソッド @DataJpaTest
Controller→Service→DBまで一気通貫 @SpringBootTest + @AutoConfigureMockMvc
本物のDB(PostgreSQLなど)で確認 @DataJpaTest か @SpringBootTest + Testcontainers

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Bootテスト入門|@WebMvcTestとMockMvcの使い分け」

Spring Boot 4.1のテスト関連の変更点

Spring Boot 4.0でテスト周りが大きく変わり、4.1も同じ形です。 3系の記事のコードをそのまま貼ると、@MockBean やパッケージ名でコンパイルエラーになります。

項目 Spring Boot 3系 Spring Boot 4.x
Beanをモックに差し替え @MockBean(3.4で非推奨) @MockitoBean(@MockBean は削除)
Beanをスパイに @SpyBean @MockitoSpyBean
@MockitoBean のパッケージ ― org.springframework.test.context.bean.override.mockito(Spring Framework側)
@WebMvcTest のパッケージ org.springframework.boot.test.autoconfigure.web.servlet org.springframework.boot.webmvc.test.autoconfigure
@AutoConfigureMockMvc 同上 org.springframework.boot.webmvc.test.autoconfigure
@DataJpaTest のパッケージ org.springframework.boot.test.autoconfigure.orm.jpa org.springframework.boot.data.jpa.test.autoconfigure
@RestClientTest のパッケージ org.springframework.boot.test.autoconfigure.web.client org.springframework.boot.restclient.test.autoconfigure
テスト用の依存 spring-boot-starter-test 1つ 技術ごとの spring-boot-starter-〇〇-test
TestRestTemplate @SpringBootTest(webEnvironment=RANDOM_PORT) で使える @AutoConfigureTestRestTemplate と spring-boot-resttestclient が必要
新しいHTTPテストクライアント ― RestTestClient(@AutoConfigureRestTestClient)
AssertJ形式のMockMvc ― MockMvcTester(AssertJがあれば自動設定)
JUnit JUnit 5 JUnit 6(@ExtendWith(SpringExtension.class) は不要のまま)
Java 17以上 17以上

移行の第一歩は「@MockBean → @MockitoBean」と「importの付け替え」です。IDEで古いimportを消して、クラス名から再度importし直すと早く直せます。

準備:依存関係(build.gradle.kts / pom.xml)

Spring Boot 4では、テストしたい技術に対応するテスト用スターターを追加します。 各テスト用スターターには、JUnit・AssertJ・Mockito・Hamcrest・JsonPathなどのテスト用ライブラリも含まれます。

// build.gradle.kts(Spring Boot 4.1)
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webmvc")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("com.h2database:h2")

    testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
    testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.withType<Test> {
    useJUnitPlatform()
}
<!-- pom.xml(抜粋) -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webmvc-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa-test</artifactId>
  <scope>test</scope>
</dependency>

テスト対象のサンプルアプリ(タスク管理API)

テストの前に、テストされる側のコードを示します。タスクを1件取得する GET /api/tasks/{id} と、登録する POST /api/tasks を持つ小さなAPIです。

// src/main/java/com/example/task/Task.java
package com.example.task;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class Task {
    @Id @GeneratedValue
    private Long id;
    private String title;
    private boolean done;

    protected Task() {}
    public Task(String title) { this.title = title; }

    public Long getId() { return id; }
    public String getTitle() { return title; }
    public boolean isDone() { return done; }
    public void complete() { this.done = true; }
}
// src/main/java/com/example/task/TaskRepository.java
package com.example.task;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TaskRepository extends JpaRepository<Task, Long> {
    List<Task> findByDoneFalseOrderByIdAsc();
}
// src/main/java/com/example/task/TaskService.java
package com.example.task;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TaskService {
    private final TaskRepository repository;

    public TaskService(TaskRepository repository) {
        this.repository = repository;
    }

    @Transactional(readOnly = true)
    public TaskResponse find(long id) {
        return repository.findById(id)
                .map(TaskResponse::from)
                .orElseThrow(() -> new TaskNotFoundException(id));
    }

    @Transactional
    public TaskResponse create(TaskRequest request) {
        return TaskResponse.from(repository.save(new Task(request.title())));
    }
}
// src/main/java/com/example/task/TaskRequest.java ほか(record)
package com.example.task;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record TaskRequest(@NotBlank @Size(max = 50) String title) {}

// TaskResponse.java
public record TaskResponse(long id, String title, boolean done) {
    static TaskResponse from(Task t) {
        return new TaskResponse(t.getId(), t.getTitle(), t.isDone());
    }
}

// TaskNotFoundException.java
public class TaskNotFoundException extends RuntimeException {
    public TaskNotFoundException(long id) { super("task not found: " + id); }
}
// src/main/java/com/example/task/TaskController.java
package com.example.task;

import jakarta.validation.Valid;
import java.net.URI;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/tasks")
public class TaskController {
    private final TaskService service;

    public TaskController(TaskService service) {
        this.service = service;
    }

    @GetMapping("/{id}")
    public TaskResponse get(@PathVariable long id) {
        return service.find(id);
    }

    @PostMapping
    public ResponseEntity<TaskResponse> create(@Valid @RequestBody TaskRequest request) {
        TaskResponse created = service.create(request);
        return ResponseEntity.created(URI.create("/api/tasks/" + created.id())).body(created);
    }

    @ExceptionHandler(TaskNotFoundException.class)
    public ProblemDetail handleNotFound(TaskNotFoundException e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
    }
}

Controllerのテスト:@WebMvcTest+MockMvc

@WebMvcTest(TaskController.class) は、指定したControllerとWeb層の部品だけを起動します。 @Service や @Repository は起動しないので、Controllerが使うServiceは @MockitoBean でモックに差し替えます。

// src/test/java/com/example/task/TaskControllerTest.java
package com.example.task;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(TaskController.class)
class TaskControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockitoBean
    TaskService taskService;

    @Test
    void 取得できる() throws Exception {
        given(taskService.find(1L)).willReturn(new TaskResponse(1L, "牛乳を買う", false));

        mockMvc.perform(get("/api/tasks/{id}", 1L))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.title").value("牛乳を買う"))
                .andExpect(jsonPath("$.done").value(false));
    }

    @Test
    void 存在しないIDは404() throws Exception {
        given(taskService.find(99L)).willThrow(new TaskNotFoundException(99L));

        mockMvc.perform(get("/api/tasks/{id}", 99L))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.detail").value("task not found: 99"));
    }

    @Test
    void 登録すると201とLocation() throws Exception {
        given(taskService.create(any())).willReturn(new TaskResponse(10L, "書類を出す", false));

        mockMvc.perform(post("/api/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"title": "書類を出す"}
                                """))
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", "/api/tasks/10"))
                .andExpect(jsonPath("$.id").value(10));
    }

    @Test
    void タイトルが空なら400でServiceは呼ばれない() throws Exception {
        mockMvc.perform(post("/api/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"title": ""}
                                """))
                .andExpect(status().isBadRequest());

        verify(taskService, never()).create(any());
    }
}

ポイントは3つです。

書き方 意味
perform(get(...)) リクエストを模擬して送る(サーバーは起動しない)
andExpect(...) ステータス・ヘッダー・JSONを検証する
andDo(print()) リクエストとレスポンスの中身をコンソールに出す(調べるときに便利)

入力チェック(@Valid)の400は、Serviceを呼ぶ前にSpringが返します。 verify(taskService, never()) で「Serviceまで届いていない」ことも確かめると、入力チェックが効いている証拠になります。

同じテストをMockMvcTesterで書く(AssertJ形式)

MockMvcTester は、MockMvcをAssertJの assertThat で書けるようにしたクラスです。 Spring Framework 6.2で追加され、AssertJがクラスパスにあればSpring Bootが自動で用意します。throws Exception が不要になり、補完で書けるのが利点です。

// src/test/java/com/example/task/TaskControllerTesterTest.java
package com.example.task;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.assertj.MockMvcTester;

@WebMvcTest(TaskController.class)
class TaskControllerTesterTest {

    @Autowired
    MockMvcTester mvc;

    @MockitoBean
    TaskService taskService;

    @Test
    void 取得できる() {
        given(taskService.find(1L)).willReturn(new TaskResponse(1L, "牛乳を買う", false));

        assertThat(mvc.get().uri("/api/tasks/{id}", 1L))
                .hasStatusOk()
                .bodyJson()
                .extractingPath("$.title").isEqualTo("牛乳を買う");
    }

    @Test
    void JSON全体を比べる() {
        given(taskService.find(1L)).willReturn(new TaskResponse(1L, "牛乳を買う", false));

        assertThat(mvc.get().uri("/api/tasks/{id}", 1L))
                .hasStatusOk()
                .bodyJson()
                .isLenientlyEqualTo("""
                        {"id": 1, "title": "牛乳を買う", "done": false}
                        """);
    }

    @Test
    void レスポンスをrecordに変換して比べる() {
        given(taskService.find(1L)).willReturn(new TaskResponse(1L, "牛乳を買う", false));

        assertThat(mvc.get().uri("/api/tasks/{id}", 1L))
                .hasStatusOk()
                .bodyJson()
                .convertTo(TaskResponse.class)
                .isEqualTo(new TaskResponse(1L, "牛乳を買う", false));
    }

    @Test
    void 登録すると201() {
        given(taskService.create(any())).willReturn(new TaskResponse(10L, "書類を出す", false));

        assertThat(mvc.post().uri("/api/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\": \"書類を出す\"}"))
                .hasStatus(HttpStatus.CREATED)
                .hasHeader("Location", "/api/tasks/10");
    }
}

MockMvcとMockMvcTesterの比較

観点 MockMvc MockMvcTester
書き方 perform(...).andExpect(...) assertThat(mvc.get()...).hasStatusOk()
例外宣言 throws Exception が必要 不要
JSONの検証 jsonPath("$.x").value(...) bodyJson().extractingPath("$.x")
JSONを型に変換 自分でObjectMapper等を使う convertTo(型.class)
失敗時のメッセージ Hamcrest形式 AssertJ形式(差分が読みやすい)
既存の資料の多さ 多い 増えつつある
おすすめの場面 既存テストに合わせる 新しく書くテスト

同じテストクラスで両方を使うこともできます。 既存のMockMvcのテストは残したまま、新しいテストからMockMvcTesterにすると無理なく移行できます。

Serviceのテスト:Springを起動せずにMockitoで

Serviceのロジックは、Springを起動しない普通のJUnit+Mockitoのテストがいちばん速く書けます。 @MockitoBean は「Springの中のBeanを差し替える」もので、Springを起動しないテストでは Mockito の @Mock を使います。Mockitoの詳しい書き方は「Mockitoの使い方」の記事にまとめています。

// src/test/java/com/example/task/TaskServiceTest.java
package com.example.task;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;

import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class TaskServiceTest {

    @Mock
    TaskRepository repository;

    @InjectMocks
    TaskService service;

    @Test
    void 見つからなければ例外() {
        given(repository.findById(5L)).willReturn(Optional.empty());

        assertThatThrownBy(() -> service.find(5L))
                .isInstanceOf(TaskNotFoundException.class)
                .hasMessage("task not found: 5");
    }

    @Test
    void 見つかればレスポンスに変換() {
        Task task = new Task("棚卸し");
        given(repository.findById(1L)).willReturn(Optional.of(task));

        TaskResponse res = service.find(1L);

        assertThat(res.title()).isEqualTo("棚卸し");
        assertThat(res.done()).isFalse();
    }
}
アノテーション 使う場所 Springの起動
@Mock(Mockito) Springを使わないテスト しない
@MockitoBean(Spring) @WebMvcTest / @SpringBootTest など する(Beanを差し替え)
@MockitoSpyBean(Spring) 本物のBeanを一部だけ差し替えたいとき する

Repositoryのテスト:@DataJpaTest

@DataJpaTest は、JPAとRepositoryだけを起動し、既定では組み込みDB(H2など)に置き換えて実行します。 各テストはトランザクション内で動き、終了時にロールバックされるので、テスト同士でデータが混ざりません。

// src/test/java/com/example/task/TaskRepositoryTest.java
package com.example.task;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;

@DataJpaTest
class TaskRepositoryTest {

    @Autowired
    TaskRepository repository;

    @Test
    void 未完了だけをID順に取得() {
        Task a = repository.save(new Task("備品を発注"));
        Task b = repository.save(new Task("会議室を予約"));
        Task c = repository.save(new Task("資料を印刷"));
        b.complete();
        repository.flush();

        List<Task> result = repository.findByDoneFalseOrderByIdAsc();

        assertThat(result)
                .extracting(Task::getTitle)
                .containsExactly("備品を発注", "資料を印刷");
    }
}

自作のクエリメソッド(findBy...、@Query)だけをテストすれば十分です。save や findById のようなSpring Dataが用意するメソッドは、テストを書かなくても動作が保証されています。

本物のDBで確かめる:Testcontainers(概要)

H2とPostgreSQLなどではSQLの方言が違うため、本番と同じDBで確かめたいときはTestcontainers(テスト中にDockerでDBを起動するライブラリ)を使います。Spring Bootの @ServiceConnection を付けると、接続情報(URL・ユーザー名など)を自動で設定してくれます。

// 概要(依存:spring-boot-testcontainers、Testcontainers の PostgreSQL モジュール、Docker が必要)
@SpringBootTest
@Testcontainers
class TaskRepositoryPostgresTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:18");

    @Autowired
    TaskRepository repository;

    // テストは @DataJpaTest の例と同じように書ける
}

Testcontainersは2系でモジュール名やパッケージが変わっています。 依存の書き方とimportは、使うバージョンの公式ドキュメントで確認してください。@DataJpaTest と組み合わせる場合は、組み込みDBへの置き換えを止める @AutoConfigureTestDatabase(replace = Replace.NONE) の指定が必要になることがあります。

全体を通すテスト:@SpringBootTest+@AutoConfigureMockMvc

Controller→Service→Repository→DBまで、モックなしで通して確かめるのが @SpringBootTest です。 既定ではサーバーを起動せず(webEnvironment = MOCK)、MockMvcを使うには @AutoConfigureMockMvc を付けます。

// src/test/java/com/example/task/TaskApiIntegrationTest.java
package com.example.task;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class TaskApiIntegrationTest {

    @Autowired
    MockMvcTester mvc;

    @Autowired
    TaskRepository repository;

    @Test
    void 登録したタスクがDBに入り取得できる() {
        assertThat(mvc.post().uri("/api/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\": \"在庫を数える\"}"))
                .hasStatus(HttpStatus.CREATED);

        assertThat(repository.findAll())
                .extracting(Task::getTitle)
                .contains("在庫を数える");
    }
}
webEnvironment 動き 使うクライアント
MOCK(既定) サーバーを起動しない MockMvc / MockMvcTester
RANDOM_PORT 空いているポートで本物のサーバーを起動 RestTestClient / TestRestTemplate
DEFINED_PORT 設定したポート(既定8080)で起動 同上
NONE Web環境なし ―

@Transactional をテストクラスに付けると、MOCK環境ではテストごとにロールバックされます。 RANDOM_PORT では、リクエストは別スレッドで処理されるためロールバックされない点に気をつけます。

そのほかのスライス:@JsonTest・@RestClientTest

@JsonTest:JSONの形だけを確かめる

package com.example.task;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;

@JsonTest
class TaskResponseJsonTest {

    @Autowired
    JacksonTester<TaskResponse> json;

    @Test
    void JSONに変換される() throws Exception {
        var content = json.write(new TaskResponse(1L, "牛乳を買う", false));

        assertThat(content).extractingJsonPathStringValue("$.title").isEqualTo("牛乳を買う");
        assertThat(content).extractingJsonPathBooleanValue("$.done").isFalse();
    }
}

日付の形式やプロパティ名の変換(@JsonProperty など)を変えたときに役立ちます。 Spring Boot 4ではJackson 3が既定になったため、独自のJSON設定を入れている場合は @JsonTest で形を確かめておくと安心です。

@RestClientTest:外部APIクライアントを確かめる

package com.example.stock;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.restclient.test.autoconfigure.RestClientTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;

@RestClientTest(StockClient.class)
class StockClientTest {

    @Autowired
    StockClient client;   // RestClient.Builder から baseUrl("https://stock.example.com") で作るクラス

    @Autowired
    MockRestServiceServer server;

    @Test
    void 在庫数を取得できる() {
        server.expect(requestTo("https://stock.example.com/stocks/A-1"))
                .andRespond(withSuccess("{\"sku\":\"A-1\",\"quantity\":5}", MediaType.APPLICATION_JSON));

        assertThat(client.quantity("A-1")).isEqualTo(5);
    }
}

MockRestServiceServer が外部APIの代わりに決まった応答を返すので、相手のサーバーなしでテストできます。 対象のクラスは、Springが用意する RestClient.Builder を受け取って RestClient を作る形にしておきます。

よく使うmatcher一覧(MockMvc / MockMvcTester)

確かめたいこと MockMvc(andExpect) MockMvcTester(assertThat)
200 OK status().isOk() .hasStatusOk()
201 Created status().isCreated() .hasStatus(HttpStatus.CREATED)
400 / 404 status().isBadRequest() / isNotFound() .hasStatus(HttpStatus.BAD_REQUEST) / .hasStatus4xxClientError()
Content-Type content().contentType(MediaType.APPLICATION_JSON) .hasContentType(MediaType.APPLICATION_JSON)
ヘッダーの値 header().string("Location", "/api/tasks/10") .hasHeader("Location", "/api/tasks/10")
JSONの1項目 jsonPath("$.title").value("牛乳を買う") .bodyJson().extractingPath("$.title").isEqualTo("牛乳を買う")
配列の件数 jsonPath("$", hasSize(2)) .bodyJson().extractingPath("$").asArray().hasSize(2)
項目がない jsonPath("$.password").doesNotExist() .bodyJson().doesNotHavePath("$.password")
項目がある jsonPath("$.id").exists() .bodyJson().hasPath("$.id")
JSON全体(順不同・余分OK) content().json("{...}") .bodyJson().isLenientlyEqualTo("{...}")
JSON全体(厳密) content().json("{...}", JsonCompareMode.STRICT) .bodyJson().isStrictlyEqualTo("{...}")
本文の文字列 content().string("ok") .hasBodyTextEqualTo("ok")
リダイレクト先 redirectedUrl("/login") .hasRedirectedUrl("/login")
画面のビュー名 view().name("tasks/list") .hasViewName("tasks/list")
モデルの属性 model().attributeExists("tasks") .model().containsKey("tasks")
中身を表示 andDo(print()) .debug()

hasSize は Hamcrest の org.hamcrest.Matchers.hasSize を static import します。JsonPathの $ はJSONのルート、$.items[0].title のように配列の要素も指定できます。

リクエストの組み立て早見表(MockMvc)

やりたいこと 書き方
パス変数 get("/api/tasks/{id}", 1L)
クエリパラメータ get("/api/tasks").param("done", "false")
JSONを送る .contentType(MediaType.APPLICATION_JSON).content("{...}")
ヘッダー .header("X-Request-Id", "abc")
Cookie .cookie(new Cookie("theme", "dark"))
ファイル送信 multipart("/upload").file(new MockMultipartFile("file", "a.csv", "text/csv", bytes))
ログイン中のユーザー(Spring Security) @WithMockUser + .with(csrf())

よくあるエラーと原因

1. No qualifying bean of type '...TaskService' available

org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'com.example.task.TaskService' available:
expected at least 1 bean which qualifies as autowire candidate.

原因:@WebMvcTest は @Service を起動しないため、ControllerがServiceを受け取れません。 テストクラスに @MockitoBean TaskService taskService; を追加します。Controllerが使うBeanが増えたら、その分の @MockitoBean も追加します。

2. Failed to load ApplicationContext

java.lang.IllegalStateException: Failed to load ApplicationContext for [WebMergedContextConfiguration ...]
...
Caused by: ...

このメッセージは「テスト用のSpringが起動できなかった」という総称で、本当の原因は下の Caused by: に書かれています。 多いのは次のとおりです。

Caused by に出る内容 原因 対処
No qualifying bean of type スライスに含まれないBeanを使っている @MockitoBean を追加
Failed to replace DataSource with an embedded database for tests @DataJpaTest 用の組み込みDBがない H2などをテスト用に追加、または @AutoConfigureTestDatabase(replace = NONE)
Could not resolve placeholder 'xxx' 設定値がテストで足りない src/test/resources/application.yml か @TestPropertySource で追加
DB接続エラー @SpringBootTest で本物のDBへ接続しようとした テスト用プロファイルでH2やTestcontainersに

3. Unable to find a @SpringBootConfiguration

java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration by searching packages upwards from the test.

原因:テストクラスのパッケージが、@SpringBootApplication を付けたクラスのパッケージ(またはその下)にありません。 テストを src/test/java/com/example/... のように、メインクラスと同じパッケージ階層に置きます。

4. @MockBean が見つからない(cannot find symbol)

error: cannot find symbol
  symbol: class MockBean

原因:Spring Boot 4で @MockBean は削除されました。 @MockitoBean(org.springframework.test.context.bean.override.mockito.MockitoBean)に置き換えます。@SpyBean は @MockitoSpyBean です。

5. そのほかのよくあるつまずき

症状 原因 対処
@SpringBootTest で MockMvc が注入できない @AutoConfigureMockMvc がない 付ける
@WebMvcTest で403が返る Spring Securityが有効 @WithMockUser と .with(csrf())(依存:spring-boot-starter-security-test)
No value at JSON path "$.title" JSONの形がテストの想定と違う andDo(print()) で実際のレスポンスを確認
@WebMvcTest が遅い・関係ないBeanまで起動する クラス指定なしで全Controllerを起動 @WebMvcTest(TaskController.class) と対象を絞る
テストごとにSpringが起動し直して遅い @MockitoBean の組み合わせがクラスごとに違う 共通の組み合わせを基底クラスや独自アノテーションにまとめる
TestRestTemplate が見つからない Boot 4でモジュールが分かれた spring-boot-resttestclient を追加し @AutoConfigureTestRestTemplate、または RestTestClient を使う

Springのテストは、同じ設定のテスト同士でApplicationContextを再利用(キャッシュ)します。 @MockitoBean の組み合わせが違うと別の設定とみなされるため、そろえるほど全体のテスト時間が短くなります。

よくある質問

Q. @WebMvcTestと@SpringBootTestはどう使い分けますか? A. Controllerの入出力(URL、ステータス、JSON、入力チェック)を確かめるなら @WebMvcTest、Controllerから DB までをつないだ動きを確かめるなら @SpringBootTest + @AutoConfigureMockMvc です。数は @WebMvcTest を多めに、@SpringBootTest は主要な流れだけにすると、テスト全体が速くなります。

Q. MockMvcとMockMvcTesterはどちらを使えばよいですか? A. 新しく書くテストは MockMvcTester がおすすめです。AssertJで書けて throws Exception が不要になり、JSONを型に変換して比べられます。既存のMockMvcのテストはそのまま動くので、同じプロジェクト内で混在させても問題ありません。

Q. @MockitoBeanと@Mockの違いは? A. @MockitoBean はSpringのApplicationContextの中のBeanをモックに差し替えます。@Mock はMockitoの機能で、Springを起動しないテストで使います。Serviceの単体テストは @Mock、@WebMvcTest などSpringを起動するテストは @MockitoBean と覚えると迷いません。

Q. Spring Boot 3のテストを4に移行するとき、最初に何をすればよいですか? A. ①spring-boot-starter-test を技術ごとのテスト用スターター(spring-boot-starter-webmvc-test など)に置き換え、②@MockBean を @MockitoBean に、③@WebMvcTest・@DataJpaTest などのimportを新しいパッケージに付け替えます。ここまでで多くのコンパイルエラーが解消します。

まとめ

  • テストしたい層だけを起動するスライス(@WebMvcTest・@DataJpaTest)を基本にし、@SpringBootTest は全体を通す要所に使う
  • Controllerは @WebMvcTest + @MockitoBean、perform/andExpect/jsonPath で入出力を確かめる
  • 新しいテストは MockMvcTester にすると、AssertJで読みやすく書ける
  • Serviceは Springを起動せず @Mock、Repositoryは自作クエリを @DataJpaTest で確かめる
  • Spring Boot 4.xでは @MockBean が削除され @MockitoBean に、テスト用スターターとパッケージ名も変わった
  • Failed to load ApplicationContext は Caused by: を読むのが解決の近道

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Bootテスト入門|@WebMvcTestとMockMvcの使い分け」

株式会社RJCでは、Spring Bootのテストの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?