- Spring
Spring Bootテスト入門|@WebMvcTestとMockMvcの使い分け
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期連続の黒字決算。