Spring Bootで「コントローラのテスト書こう!」と思ったのに、いきなり手が止まってませんか?
「MockMvcって何者?サーバー起動いるの?」「@WebMvcTestと@SpringBootTest、どっち使うの?」「JSON投げたいのに書き方が分からない…」みたいなやつです。さらにSpring Securityを入れてると、テストが401/403で落ちて終わりになりがちで、心が折れますよね…。
このページは、そういう“つまずきあるある”を全部まとめて、最速で動く形にしていきます。やることはシンプルです。
まずは GETで200が返る最小テストを作って「動いた!」を最短で取ります。そこから、JSONのPOST/PUT → バリデーションで400 → 例外のテスト → Securityの401/403/CSRF対策まで、順番にテンプレ化していきます。
先に結論だけ言うと、基本はこれです。
速さ重視でコントローラだけ見るなら @WebMvcTest、
設定含めて全体も確かめたいなら @SpringBootTest(+ MockMvc設定)。
この使い分けが分かると、テストが一気にラクになりますよ。
まず結論:このページでできること
いきなり結論からいきますね。
このページであなたができるようになるのは、「サーバーを起動せずに」Spring Bootのコントローラテストを速く・確実に書くことです。
しかも、GETで200が返るだけじゃなくて、実務で詰まりがちな JSON送信/バリデーション/例外/Spring Security(401/403/CSRF)まで、テンプレとして持ち帰れます。
「でもテストって、全部起動して確認するんじゃないの?」って思いますよね。
そこで出てくるのが MockMvc と、@WebMvcTest / @SpringBootTest の使い分けです。
MockMvcって何?サーバー起動は必要?
MockMvcは一言でいうと、“サーバーを立てずに、コントローラに疑似リクエストを投げる道具”です。
ブラウザやPostmanみたいにHTTPで叩く雰囲気はそのままに、テストの中で GET /api/... とか POST /api/... を投げて、返ってきたステータスやJSONをチェックできます。
ポイントはここです。
つまり、コントローラテストの「最初の一歩」にちょうどいい相棒です。
速さ重視は@WebMvcTest/全体確認は@SpringBootTest
ここも一言で決めちゃいます。
@WebMvcTest は “Webまわりだけ” を読み込むので、起動がめちゃ速いです。その代わり、Controllerが呼ぶServiceとかは基本的に読み込まれないので、そこは後で @MockBean で差し替えます(この話はH2-4でバッチリやります)。
@SpringBootTest は逆で、アプリをごっそり起動します。だから設定ミス(Security設定やBean定義の抜け)に気づきやすいんですが、当然その分重いです。
「速さ」と「安心」のトレードオフ、って感じですね。
使い分け早見表(迷ったらここを見ましょう)
迷ったら、いったんこれだけ見てください。
| 目的 | 使うやつ | メリット | デメリット | 向いてる場面 |
|---|---|---|---|---|
| とにかく速く、コントローラの入出力を固めたい | @WebMvcTest | 速い/失敗原因が絞りやすい | Service/Repositoryは自分で用意(モック) | Controllerの分岐、バリデーション、エラーJSONの形 |
| 設定込みで「全体として動くか」も見たい | @SpringBootTest(+ MockMvc設定) | 実運用に近い/設定ミスに強い | 遅い/DBや外部連携があるとさらに重い | Security設定確認、Bean設定の整合性、結合テスト寄りの確認 |
結論、最速ルートはこうです。
- まず
@WebMvcTestで GETの200 を通す - 次に JSON / バリデ / 例外 をテンプレ化する
- 最後に必要なところだけ
@SpringBootTestで 全体確認する

次の章では、ほんとに「コピペで動く」最小サンプルからいきますね。
まず動かす:GETで200を確認する最小サンプル
ここは「理屈より先に、まず1回成功」させます。
GETを投げて200が返る。まずこれだけ通せると、コントローラテストの怖さが一気に減りますよ。
依存関係(まず足すもの)とテストの形
まずは依存関係です。Securityを使ってないなら spring-boot-starter-test だけでOK。
Securityありなら、あとで使うので spring-security-test も入れておくと安心です。
Gradle(build.gradle)
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test' // Security使うなら
}
Maven(pom.xml)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Security使うなら -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
次に、テスト対象のコントローラの超ミニ例です(すでにある人は読み飛ばしてOKです)。
@RestController
@RequestMapping("/hello")
class HelloController {
@GetMapping
String hello() {
return "ok";
}
}
MockMvcの注入(@Autowired)と実行(perform)
はい、ここが「最小で動く」テストです。これをコピペしてまず動かしてください。
@WebMvcTest(HelloController.class)
class HelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void get_hello_returns200() throws Exception {
mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andDo(print());
}
}
ポイントは3つだけです。
ちなみに
get("/hello")はMockMvcRequestBuilders.get()のやつです。
import はIDEの自動で入ることが多いですが、手動ならstatic importを使うのが定番です。
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
失敗したら最初に見るポイント(printで中身を見る)
もしここで落ちたら、まず見るのはこれです。printが出してくれる情報が超ヒントになります。
よくある「最初のつまずき」も置いておきますね。
- 404になる:パスが違う(
/helloじゃなくて/api/helloだった等) - 401/403になる:Securityが効いてる(これはH2-8で対策します。今は“そういうこともある”でOK)
- テストが動かない:
@WebMvcTestの対象クラス指定が違う/テストクラスの場所が変(パッケージ構成)
ここまでで「GETの200」が通れば、もう勝ちです。

次は実務で毎回使う リクエストの作り方(param/header/JSON) と 結果の見方(jsonPath) をテンプレにしていきます。
よく使う3点セット:リクエスト作成と結果チェックの基本
MockMvcのテストは、だいたいこの「型」で回せます。
- 入れる(param/header/json)
- 送る(perform)
- 見る(andExpect)
この型をテンプレ化すると、毎回迷子にならないです。
リクエスト:param / header / contentType / accept
mockMvc.perform(get("/users")
.param("q", "taro")
.header("X-Trace-Id", "test-123")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
JSONのPOST/PUT:ObjectMapperで文字列にする
JSONを送るときは、DTO → ObjectMapperでJSON文字列にして content() に入れるのが定番です。
@Autowired MockMvc mockMvc;
@Autowired ObjectMapper objectMapper;
@Test
void post_user_creates() throws Exception {
var req = new CreateUserRequest("taro", 10);
String json = objectMapper.writeValueAsString(req);
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isCreated());
}
※ contentType を忘れると「JSONとして読まれない」事故が起きやすいので、ここは固定でOKです。
結果:status / header / jsonPath / redirect の見方
mockMvc.perform(get("/users/1").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(header().string("Content-Type", "application/json"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("taro"));
リダイレクト例(302のとき):
mockMvc.perform(get("/old-page"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/new-page"));
コピペ用テンプレ(GET/POST/PUTの型)
GET(param + json確認)
@Test
void get_template() throws Exception {
mockMvc.perform(get("/api/items")
.param("page", "1")
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.items").isArray());
}
POST(JSON送る)
@Test
void post_template() throws Exception {
var req = new AnyRequest("value");
mockMvc.perform(post("/api/items")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andDo(print())
.andExpect(status().isCreated());
}
PUT(更新の定番)
@Test
void put_template() throws Exception {
var req = new AnyRequest("newValue");
mockMvc.perform(put("/api/items/1")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andDo(print())
.andExpect(status().isOk());
}
ここまでできると、テストはもう「書ける側」です。次(H2-4)で、@WebMvcTest でほぼ確実に踏む 「Serviceが見つからない問題」 を先に潰します。
@WebMvcTestで詰まる所を先に潰す(Serviceが見つからない問題)
@WebMvcTest を使ってテストを書いたら、いきなりこう言われたことありませんか?
結論:それ、正常です。(びっくりしますよね…)
なぜServiceがない?(テスト範囲が“コントローラだけ”)
@WebMvcTest は、ざっくり言うと “Webまわりだけを読み込むテスト”です。
でも、逆にこういうのは 基本読み込まれません。
だから、Controllerが UserService を @Autowired してたら、テスト側で用意しない限り「いないよ!」って落ちます。
@MockBeanで置きかえる(戻り値を決める)
解決はシンプルで、Controllerが使うServiceを @MockBeanで差し替えます。
そして「この入力なら、Serviceはこれを返す」をテストで決めます。
例として、こんなControllerを想像してください。
@RestController
@RequestMapping("/users")
class UserController {
private final UserService userService;
UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
UserResponse get(@PathVariable long id) {
return userService.findById(id);
}
}
テストはこうします。
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService;
@Test
void get_user_returns200() throws Exception {
// Serviceの戻り値を決める
when(userService.findById(1L))
.thenReturn(new UserResponse(1L, "taro"));
mockMvc.perform(get("/users/1").accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("taro"));
}
}
これでControllerは「本物のService」じゃなくて、「テストが用意したニセService」を使って動いてくれます。
さらに、例外パターンも作れます。ControllerAdviceと合わせると超強いです(H2-7でやります)。
@Test
void get_user_notFound_returns404() throws Exception {
when(userService.findById(999L))
.thenThrow(new UserNotFoundException("not found"));
mockMvc.perform(get("/users/999").accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isNotFound());
}
どこまでモック?判断ルール(迷いを減らす)
「モックってどこまでやるのが正解?」で迷いがちなので、ここはルールで割り切るのが楽です。
基本ルール:Controllerテストは“Controllerの責任範囲だけ”見る
- ✅ 見る:URL、パラメータ、ヘッダ、JSON、ステータス、レスポンス形、バリデーション、例外ハンドリング
- ❌ 見ない:Serviceのビジネスロジック、DBの中身、外部APIの実通信
なので、迷ったらこう考えるとスッキリします。
「ControllerとServiceのつなぎ込みも含めて確認したい」なら、@SpringBootTest + @AutoConfigureMockMvc の出番です。

ここまでで、@WebMvcTest の地雷はかなり避けられます。
次は、「もうちょい本物寄りに、全体も確かめたい」人向けに @SpringBootTestの使いどころをまとめます。
“結合テストっぽく”全体も確かめたい:@SpringBootTest + @AutoConfigureMockMvc
「Controllerだけじゃなくて、設定込みでちゃんと動くかも見たい…」ってときはこれです。
@SpringBootTest はアプリを“ほぼ丸ごと”読み込むので、Beanのつなぎ忘れやSecurity/設定のズレに気づきやすいです
その代わり、@WebMvcTest より だいぶ重いです。なので基本は「本数を絞って使う」のがコツです。
何が増える?(設定ミスに気づきやすい)
サーバー起動は不要で、MockMvcを使うならこの形が定番です。
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIT {
@Autowired MockMvc mockMvc;
@Test
void get_returns200() throws Exception {
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk());
}
}
DBがある場合の考え方(軽くするコツ)
DBや外部APIまで全部“本物”にすると一気に重くなります。まずは次のどれかで軽量化が楽です。
どっちをいつ使う?再チェック
迷ったらこの運用がいちばん安定です。

次は入力がダメならちゃんと400に落とす(バリデーション)テストに取り組みます。
バリデーションのテスト:入力がダメなら400にする
「入力が変だったら落とす」は、仕様としてテストで固定しちゃうのが一番ラクです。
ここでは @Valid で弾いて、400(Bad Request)になるところまでをテンプレ化します。
@Validの基本と、落ちる入力の作り方
まずは最小イメージです。@RequestBody に @Valid を付けると、バリデーションに引っかかった時点で コントローラ本体に入る前に400になります。
// 例:DTO(recordでもclassでもOK)
public record CreateUserRequest(
@NotBlank String name,
@NotNull @Min(0) Integer age
) {}
@PostMapping("/users")
ResponseEntity<Void> create(@Valid @RequestBody CreateUserRequest req) {
return ResponseEntity.status(HttpStatus.CREATED).build();
}
ダメ入力カタログ(テストでよく作るやつ)
エラーメッセージをjsonPathで確認する型
まずは「落ちた」ことを固定して、そのあと中身を確認します。
@Test
void validation_error_returns400() throws Exception {
var bad = Map.of("name", "", "age", -1);
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(bad)))
.andDo(print())
.andExpect(status().isBadRequest());
}
エラー内容のチェックは、あなたのアプリのエラーフォーマットに合わせて jsonPath を当てます。迷ったら print() のレスポンスを見て合わせればOKです。
例(errors 配列を返す設計のとき):
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors[0].field").isNotEmpty());
例(ProblemDetail系っぽい形のとき):
.andExpect(jsonPath("$.title").exists())
.andExpect(jsonPath("$.status").value(400));
よくある落とし穴(Content-Type、文字コード、必須項目)
ここで詰まりやすいのはだいたいこの3つです。

次は、バリデじゃなくて Serviceが例外を投げたときに、狙ったエラーJSONで返せてるかをテストに取り組みます。
例外のテスト:想定どおりのエラーにできているか
「落ちたとき、どう返すか」って、放っておくと地獄になりがちです。
たとえばServiceで例外が起きたのに、レスポンスが毎回バラバラだと、フロントも他API利用者も困りますよね。なのでここは、落ち方(ステータス+エラーボディ)を仕様として固定して、テストで守ります。
Serviceが例外→ControllerAdviceで整形、をテストする
よくある形はこれです。
例:見つからない用の例外
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) { super(message); }
}
例:エラーの返し方を固定するAdvice(超シンプル版)
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
ResponseEntity<Map<String, Object>> handle(UserNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of(
"code", "USER_NOT_FOUND",
"message", ex.getMessage()
));
}
}
この「code/messageみたいな形」を先に決めると、あとがめちゃ楽です。
ステータスとボディ(エラーコード等)を固定する
テストでは、Serviceを例外を投げるようにモックして、ControllerAdviceの整形結果を確認します。
ポイント:@WebMvcTest だとAdviceが自動で入る場合もありますが、確実にしたいなら @Import で読み込みます。
@WebMvcTest(UserController.class)
@Import(ApiExceptionHandler.class)
class UserControllerExceptionTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService;
@Test
void when_notFound_then_404_and_errorBody() throws Exception {
when(userService.findById(999L))
.thenThrow(new UserNotFoundException("user not found"));
mockMvc.perform(get("/users/999").accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("USER_NOT_FOUND"))
.andExpect(jsonPath("$.message").value("user not found"));
}
}
ここまでできれば、「落ち方」が仕様として固まりました。
例外テストのテンプレ(1パターン作れば増やせる)
例外テストは、基本この型で量産できます。
- Serviceを thenThrow にする
status()を決め打ちする(404/409/500…)jsonPathで code/message を固定する(必要ならdetailsも)
@Test
void exception_template() throws Exception {
when(service.someMethod()).thenThrow(new RuntimeException("boom"));
mockMvc.perform(get("/api/xxx").accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().is5xxServerError())
.andExpect(jsonPath("$.code").exists())
.andExpect(jsonPath("$.message").exists());
}

次は、テストが落ちる最大要因になりがちな Spring Securityの401/403/CSRFを、チェックリストで一気に片付けます。
Spring Securityで401/403になった時の定番パターン
ここ、コントローラテスト最大の罠です。テストが落ちたら、まずはこのチェックリストで切り分けると最短で直せます。
まず切り分け:401(未ログイン)と403(権限/CSRF)
なので順番はこれでOKです。
- ログインしてる?
- 権限合ってる?
- POST系ならCSRF付けた?
テスト用ログイン:@WithMockUser と user() の使い分け
どっちも「ログインしてる状態」を作れます。違いは“楽さ”です。
@WithMockUser(いちばん楽)
@WithMockUser(username = "taro", roles = "USER")
@Test
void secured_api_ok() throws Exception {
mockMvc.perform(get("/secure").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
user()(この1本だけ管理者にしたい、みたいな時)
mockMvc.perform(get("/admin")
.with(user("hanako").roles("ADMIN")))
.andExpect(status().isOk());
使い分け目安:
だいたい同じユーザーで回すなら @WithMockUser、テスト中で切り替えるなら user() が楽です。
POST/PUT/DELETEで403?→CSRFの付け方(csrf())
「ログインしてるのに403」だと、次に疑うのがCSRFです。
特に POST/PUT/DELETE は、CSRFが有効だとトークン無しで落ちがちです。
@WithMockUser
@Test
void post_needs_csrf() throws Exception {
mockMvc.perform(post("/users")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"taro\",\"age\":10}"))
.andExpect(status().isCreated());
}
認証が目的じゃない時だけ:フィルタ無効(addFilters=false)の注意点
どうしても「今回はSecurityの動きは見なくていい、ControllerのJSONだけ見たい」って時は、フィルタを切る手もあります。
@SpringBootTest
@AutoConfigureMockMvc(addFilters = false)
class NoSecurityIT { /* ... */ }
ただし注意です。これは “Securityをテストしてない状態” になります。
なので使うのは「認証・権限の確認が目的じゃないテスト」だけにして、Securityのテストは別でちゃんと @WithMockUser / csrf() 付きで残すのがおすすめです。

次は、落ちたときに自力で進むための print()中心のデバッグ小ワザをまとめます。
失敗したときのデバッグ小ワザ&次の一歩
テストが落ちたとき、気持ちは分かります。
でも大丈夫です。MockMvcは「どこがズレたか」を見つける道具がちゃんと揃ってます。ここは“道具箱”として覚えちゃいましょう。
.andDo(print())/レスポンスを直接見る
最強はこれです。迷ったらまず付けます。
mockMvc.perform(get("/users/1"))
.andDo(print())
.andExpect(status().isOk());
printで見えるのは、だいたいこのへんです。
つまり、「想像」じゃなくて「事実」を見るためのやつです。
期待値を増やす順番(status→json→header)
テストは、いきなり全部当てに行くと沼ります。おすすめ順番はこれです。
- statusだけ当てる(まず合ってるか)
- 次に jsonPathを1個だけ足す(要点だけ)
- 最後に headerとか細かい条件を増やす
例:
mockMvc.perform(get("/users/1").accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk()) // ①まずここ
.andExpect(jsonPath("$.id").value(1)) // ②次に要点
.andExpect(jsonPath("$.name").value("taro"));// ③増やす
こうすると、ズレた瞬間に「どこがズレたか」が分かりやすいです。
追加ネタ:MockMvcTester
MockMvcは慣れると強いんですが、チェーンが長くなると読みづらいこともあります。
そんなときに選択肢になるのが MockMvcTester みたいな「読みやすさ寄り」の書き方です。
ポイントは「今の書き方に慣れてからでもOK」です。
まずは本記事のテンプレ(perform + andExpect + jsonPath)で安定させて、チームの好みに合わせて読みやすい形に寄せるのが安全です。
まとめ:あなたは次に何を足す?
最後に、状況別の「次これやればOK」を置いて終わります。
ここまで来たら、あなたはもう「コントローラテスト書ける人」です。
あとは、あなたのプロジェクトのAPIにこのテンプレを当てはめて、テストを“増やすだけ”にしていきましょう。
よくある質問
- QMockMvcって、ほんとにサーバー起動しなくていいんですか?
- A
はい、基本いりません。MockMvcは疑似リクエストをアプリ内部に投げる仕組みなので、Tomcatを立てずにテストできます。その分めっちゃ速いです。
- Q
@WebMvcTestと@SpringBootTest、結局どっちを使えばいいですか? - A
迷ったらこうです。
普段は
@WebMvcTest多め、最後に@SpringBootTest数本、が安定します。
- Q
@WebMvcTestで「Serviceが見つからない」って怒られます… - A
それ正常です。
@WebMvcTestはコントローラ周りだけ読み込むので、Serviceは基本いません。テスト側で@MockBeanを置いて差し替えてください。
- QJSONのPOSTを送ったのに、なぜか400/415になります…
- A
だいたい
contentType(application/json)を付け忘れです。content()にJSON文字列を入れて、contentType(MediaType.APPLICATION_JSON)を固定で付けるのが安全です。
- Q
ObjectMapperを@Autowiredできません。どうすれば? - A
まずはテストがSpringのコンテキストで動いているか確認です(
@WebMvcTest/@SpringBootTestが付いてるか)。それでも無理なら、一旦new ObjectMapper()でも動きますが、基本はSpringのObjectMapperを使うのが無難です(設定が揃うので)。
- Q
jsonPathの書き方がよく分かりません… - A
超ざっくりこれだけ覚えればOKです。
迷ったら
.andDo(print())で返ってきたJSONを見て、その形に合わせて書くのが最短です。
- Qバリデーション(@Valid)のテストって、何を確認すればいいですか?
- A
まずは 400になることを固定すればOKです。余裕があれば、エラーレスポンスの形(
codeやerrors配列など)もjsonPathで確認して「仕様」にしちゃうと強いです。
- Q例外のテストは、ControllerとServiceどっちをテストしてるんですか?
- A
コントローラテストでは、基本はこうです。
Serviceはモックで例外を投げさせて、ControllerAdviceが“狙い通りのエラー形式”を返すかを確認します。
「落ち方(statusとエラーJSON)」を固定するのが目的です。
- QSecurityを入れたらテストが401/403で全部落ちました…
- A
まず切り分けです。
この順で見ると早いです。
- Q
@AutoConfigureMockMvc(addFilters=false)って使っていいんですか? - A
使っていいけど、目的が「認証の確認じゃない」場合だけにした方が安全です。フィルタを無効にするとSecurityのチェックをすっ飛ばすので、「Security込みの正しさ」を確認したいテストでは使わないのがおすすめです。
