スポンサーリンク

サーバー起動なしでOK!Spring Bootコントローラテスト入門(MockMvc/認証つき)

スポンサーリンク
この記事は約34分で読めます。
スポンサーリンク

Spring Bootで「コントローラのテスト書こう!」と思ったのに、いきなり手が止まってませんか?
「MockMvcって何者?サーバー起動いるの?」「@WebMvcTestと@SpringBootTest、どっち使うの?」「JSON投げたいのに書き方が分からない…」みたいなやつです。さらにSpring Securityを入れてると、テストが401/403で落ちて終わりになりがちで、心が折れますよね…。

このページは、そういう“つまずきあるある”を全部まとめて、最速で動く形にしていきます。やることはシンプルです。
まずは GETで200が返る最小テストを作って「動いた!」を最短で取ります。そこから、JSONのPOST/PUT → バリデーションで400 → 例外のテスト → Securityの401/403/CSRF対策まで、順番にテンプレ化していきます。

先に結論だけ言うと、基本はこれです。
速さ重視でコントローラだけ見るなら @WebMvcTest
設定含めて全体も確かめたいなら @SpringBootTest(+ MockMvc設定)
この使い分けが分かると、テストが一気にラクになりますよ。

  1. まず結論:このページでできること
    1. MockMvcって何?サーバー起動は必要?
    2. 速さ重視は@WebMvcTest/全体確認は@SpringBootTest
    3. 使い分け早見表(迷ったらここを見ましょう)
  2. まず動かす:GETで200を確認する最小サンプル
    1. 依存関係(まず足すもの)とテストの形
    2. MockMvcの注入(@Autowired)と実行(perform)
    3. 失敗したら最初に見るポイント(printで中身を見る)
  3. よく使う3点セット:リクエスト作成と結果チェックの基本
    1. リクエスト:param / header / contentType / accept
    2. JSONのPOST/PUT:ObjectMapperで文字列にする
    3. 結果:status / header / jsonPath / redirect の見方
    4. コピペ用テンプレ(GET/POST/PUTの型)
  4. @WebMvcTestで詰まる所を先に潰す(Serviceが見つからない問題)
    1. なぜServiceがない?(テスト範囲が“コントローラだけ”)
    2. @MockBeanで置きかえる(戻り値を決める)
    3. どこまでモック?判断ルール(迷いを減らす)
  5. “結合テストっぽく”全体も確かめたい:@SpringBootTest + @AutoConfigureMockMvc
    1. 何が増える?(設定ミスに気づきやすい)
    2. DBがある場合の考え方(軽くするコツ)
    3. どっちをいつ使う?再チェック
  6. バリデーションのテスト:入力がダメなら400にする
    1. @Validの基本と、落ちる入力の作り方
    2. エラーメッセージをjsonPathで確認する型
    3. よくある落とし穴(Content-Type、文字コード、必須項目)
  7. 例外のテスト:想定どおりのエラーにできているか
    1. Serviceが例外→ControllerAdviceで整形、をテストする
    2. ステータスとボディ(エラーコード等)を固定する
    3. 例外テストのテンプレ(1パターン作れば増やせる)
  8. Spring Securityで401/403になった時の定番パターン
    1. まず切り分け:401(未ログイン)と403(権限/CSRF)
    2. テスト用ログイン:@WithMockUser と user() の使い分け
    3. POST/PUT/DELETEで403?→CSRFの付け方(csrf())
    4. 認証が目的じゃない時だけ:フィルタ無効(addFilters=false)の注意点
  9. 失敗したときのデバッグ小ワザ&次の一歩
    1. .andDo(print())/レスポンスを直接見る
    2. 期待値を増やす順番(status→json→header)
    3. 追加ネタ:MockMvcTester
    4. まとめ:あなたは次に何を足す?
  10. よくある質問
スポンサーリンク

まず結論:このページでできること

いきなり結論からいきますね。

このページであなたができるようになるのは、「サーバーを起動せずに」Spring Bootのコントローラテストを速く・確実に書くことです。

しかも、GETで200が返るだけじゃなくて、実務で詰まりがちな JSON送信/バリデーション/例外/Spring Security(401/403/CSRF)まで、テンプレとして持ち帰れます。

「でもテストって、全部起動して確認するんじゃないの?」って思いますよね。
そこで出てくるのが MockMvc と、@WebMvcTest / @SpringBootTest の使い分けです。

MockMvcって何?サーバー起動は必要?

MockMvcは一言でいうと、“サーバーを立てずに、コントローラに疑似リクエストを投げる道具”です。

ブラウザやPostmanみたいにHTTPで叩く雰囲気はそのままに、テストの中で GET /api/... とか POST /api/... を投げて、返ってきたステータスやJSONをチェックできます。

ポイントはここです。

  • サーバー起動なしでテストできる(だから速い)
  • リクエスト〜レスポンスの形をそのまま確認できる(JSONも見れる)
  • 失敗したら レスポンスの中身をprintで確認できる(デバッグが楽)

つまり、コントローラテストの「最初の一歩」にちょうどいい相棒です。

速さ重視は@WebMvcTest/全体確認は@SpringBootTest

ここも一言で決めちゃいます。

  • 速さ優先で、コントローラの動きだけ見たい@WebMvcTest
  • アプリ全体の設定も含めて、ちゃんと動くか見たい@SpringBootTest

@WebMvcTest は “Webまわりだけ” を読み込むので、起動がめちゃ速いです。その代わり、Controllerが呼ぶServiceとかは基本的に読み込まれないので、そこは後で @MockBean で差し替えます(この話はH2-4でバッチリやります)。

@SpringBootTest は逆で、アプリをごっそり起動します。だから設定ミス(Security設定やBean定義の抜け)に気づきやすいんですが、当然その分重いです。
「速さ」と「安心」のトレードオフ、って感じですね。

使い分け早見表(迷ったらここを見ましょう)

迷ったら、いったんこれだけ見てください。

目的使うやつメリットデメリット向いてる場面
とにかく速く、コントローラの入出力を固めたい@WebMvcTest速い/失敗原因が絞りやすいService/Repositoryは自分で用意(モック)Controllerの分岐、バリデーション、エラーJSONの形
設定込みで「全体として動くか」も見たい@SpringBootTest(+ MockMvc設定)実運用に近い/設定ミスに強い遅い/DBや外部連携があるとさらに重いSecurity設定確認、Bean設定の整合性、結合テスト寄りの確認

結論、最速ルートはこうです。

  • まず @WebMvcTestGETの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つだけです。

  • @WebMvcTest(HelloController.class)このコントローラだけを対象にする(速い)
  • MockMvc@Autowired で受け取る
  • perform → andExpect(status) → andDo(print()) の流れを固定する

ちなみに 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が出してくれる情報が超ヒントになります。

  • どのURLに投げたか(Request URI)
  • ステータスが何か(200じゃなくて404/401/403とか)
  • レスポンスボディ(何が返ってきたか)
  • 例外(Handler/Resolved Exception)

よくある「最初のつまずき」も置いておきますね。

  • 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

  • param():クエリやフォームっぽい値を入れる
  • header():認証ヘッダや独自ヘッダを入れる
  • contentType():送る中身の種類(JSONなら application/json
  • accept():欲しい戻り値の種類(JSON欲しいなら application/json
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 の見方

  • status():まず最初はこれだけ当てる(200/201/400/401/403…)
  • header():Locationやトークンなど
  • jsonPath():JSONの中身チェック(いちばん使います)
  • redirectedUrl():302のときの移動先(画面系やログイン系で出やすい)
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 を使ってテストを書いたら、いきなりこう言われたことありませんか?

  • NoSuchBeanDefinitionException とか出て、Serviceが見つからない!」
  • 「Controllerはいるのに、依存してるやつがいない…」

結論:それ、正常です。(びっくりしますよね…)

なぜServiceがない?(テスト範囲が“コントローラだけ”)

@WebMvcTest は、ざっくり言うと “Webまわりだけを読み込むテスト”です。

  • Controller(と、リクエスト/レスポンス変換、バリデーション周り)
  • Spring MVCの設定
  • (だいたい)ControllerAdvice などのWeb寄り

でも、逆にこういうのは 基本読み込まれません

  • Service
  • Repository(DB周り)
  • たいていの @Component(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がやるのは「計算・判断・保存」
    それはServiceの単体テストや結合テストで見る

「ControllerとServiceのつなぎ込みも含めて確認したい」なら、@SpringBootTest + @AutoConfigureMockMvc の出番です。

筆者
筆者

ここまでで、@WebMvcTest の地雷はかなり避けられます。
次は、「もうちょい本物寄りに、全体も確かめたい」人向けに @SpringBootTestの使いどころをまとめます。

“結合テストっぽく”全体も確かめたい:@SpringBootTest + @AutoConfigureMockMvc

「Controllerだけじゃなくて、設定込みでちゃんと動くかも見たい…」ってときはこれです。

@SpringBootTest はアプリを“ほぼ丸ごと”読み込むので、Beanのつなぎ忘れSecurity/設定のズレに気づきやすいです

その代わり、@WebMvcTest より だいぶ重いです。なので基本は「本数を絞って使う」のがコツです。

何が増える?(設定ミスに気づきやすい)

  • @WebMvcTest:Controller中心(速い)
  • @SpringBootTest:アプリ全体(広い=ミスが見つかる、でも遅い)

サーバー起動は不要で、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まで全部“本物”にすると一気に重くなります。まずは次のどれかで軽量化が楽です。

  • DBは テスト用(インメモリ) にする、または Testcontainers を使う
  • 外部APIは @MockBean で差し替える(通信はしない)
  • 「Controllerの確認」だけなら、DBに依存するテストは本数を絞る

どっちをいつ使う?再チェック

迷ったらこの運用がいちばん安定です。

  • ふだんは @WebMvcTestで速く大量に(分岐・バリデ・例外・レスポンス形)
  • 最後に @SpringBootTestを数本だけ(設定やSecurity込みの“通しチェック”)
筆者
筆者

次は入力がダメならちゃんと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();
}

ダメ入力カタログ(テストでよく作るやつ)

  • 空文字("") / 空白だけ(" "
  • 必須なし(フィールドごと消す or null
  • 長すぎ(@Size(max=...) で落とす)
  • 型違い(数字のところに "abc"

エラーメッセージを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つです。

  • contentType(application/json) を忘れる → JSONとして読まれず、別のエラーになることあり
  • 必須項目の“落とし方”がズレる
    • フィールドを消す:@NotNull が効く
    • 空文字にする:@NotBlank / @NotEmpty が効く
  • 型違いで落ちる(例:age: "abc"
    → バリデーション前に変換エラーになるので、返るエラー形式が変わりがちです(ここも print() が正義です)
筆者
筆者

次は、バリデじゃなくて Serviceが例外を投げたときに、狙ったエラーJSONで返せてるかをテストに取り組みます。

例外のテスト:想定どおりのエラーにできているか

「落ちたとき、どう返すか」って、放っておくと地獄になりがちです。
たとえばServiceで例外が起きたのに、レスポンスが毎回バラバラだと、フロントも他API利用者も困りますよね。なのでここは、落ち方(ステータス+エラーボディ)を仕様として固定して、テストで守ります。

Serviceが例外→ControllerAdviceで整形、をテストする

よくある形はこれです。

  • Serviceが「見つからない」とか「権限ない」とかで例外を投げる
  • Controllerはそれを受けて…じゃなくて、@RestControllerAdviceがまとめて整形して返す

例:見つからない用の例外

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…)
  • jsonPathcode/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)

  • 401:ログインしてない(= 認証情報がない)
  • 403:ログインはしてるけどダメ(= 権限が足りない or CSRFがない

なので順番はこれでOKです。

  • ログインしてる?
  • 権限合ってる?
  • POST系ならCSRF付けた?

テスト用ログイン:@WithMockUser と user() の使い分け

どっちも「ログインしてる状態」を作れます。違いは“楽さ”です。

  • @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で見えるのは、だいたいこのへんです。

  • 実際に叩いたURL、パラメータ、ヘッダ
  • ステータス(200じゃなくて401/403/404/500とか)
  • レスポンスボディ(JSONの中身)
  • 例外(何が原因で落ちたか)

つまり、「想像」じゃなくて「事実」を見るためのやつです。

期待値を増やす順番(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」を置いて終わります。

  • まだGETの200が通らない
    ここに戻って print() で 404/401/403 を見て原因を潰す
  • JSONのPOST/PUTを増やしたい
    ここのテンプレをコピペして、ObjectMapperjsonPath を足す
  • @WebMvcTestでServiceがないって怒られる
    ここ@MockBean を入れて、戻り値/例外を作る
  • バリデーションや例外の“落ち方”を固めたい
    ここここの型で、400/404/409などを仕様として固定する
  • Securityで401/403が止まらない
    ここの順番(ログイン→権限→CSRF)でチェックする

ここまで来たら、あなたはもう「コントローラテスト書ける人」です。
あとは、あなたのプロジェクトのAPIにこのテンプレを当てはめて、テストを“増やすだけ”にしていきましょう。

よくある質問

Q
MockMvcって、ほんとにサーバー起動しなくていいんですか?
A

はい、基本いりません。MockMvcは疑似リクエストをアプリ内部に投げる仕組みなので、Tomcatを立てずにテストできます。その分めっちゃ速いです。

Q
@WebMvcTest@SpringBootTest、結局どっちを使えばいいですか?
A

迷ったらこうです。

  • 速さ&コントローラの入出力だけ固めたい@WebMvcTest
  • 設定込みで全体も動くか見たい@SpringBootTest + @AutoConfigureMockMvc

普段は @WebMvcTest 多め、最後に @SpringBootTest 数本、が安定します。

Q
@WebMvcTest で「Serviceが見つからない」って怒られます…
A

それ正常です。@WebMvcTestコントローラ周りだけ読み込むので、Serviceは基本いません。テスト側で @MockBean を置いて差し替えてください。

Q
JSONの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です。

  • $.id → ルートの id
  • $.items[0].name → 配列の0番目の name

迷ったら .andDo(print()) で返ってきたJSONを見て、その形に合わせて書くのが最短です。

Q
バリデーション(@Valid)のテストって、何を確認すればいいですか?
A

まずは 400になることを固定すればOKです。余裕があれば、エラーレスポンスの形(codeerrors 配列など)も jsonPath で確認して「仕様」にしちゃうと強いです。

Q
例外のテストは、ControllerとServiceどっちをテストしてるんですか?
A

コントローラテストでは、基本はこうです。
Serviceはモックで例外を投げさせて、ControllerAdviceが“狙い通りのエラー形式”を返すかを確認します。
「落ち方(statusとエラーJSON)」を固定するのが目的です。

Q
Securityを入れたらテストが401/403で全部落ちました…
A

まず切り分けです。

  • 401:未ログイン → @WithMockUser.with(user("...")) を付ける
  • 403:権限 or CSRF → 権限を合わせる/POST系なら .with(csrf()) を付ける

この順で見ると早いです。

Q
@AutoConfigureMockMvc(addFilters=false) って使っていいんですか?
A

使っていいけど、目的が「認証の確認じゃない」場合だけにした方が安全です。フィルタを無効にするとSecurityのチェックをすっ飛ばすので、「Security込みの正しさ」を確認したいテストでは使わないのがおすすめです。

タイトルとURLをコピーしました