ファイルアップロードAPIを実装したものの、MockMultipartFile での正常系テストの書き方や、サイズ上限超過・不正なMIMEタイプで400が返ることの検証方法が分からず手が止まった人は多いはずです。

この記事では MockMultipartFile と perform(multipart()) によるアップロードAPIのテストに絞って解説します。ダウンロードAPIのテストはMockMvcでファイルダウンロードAPIをテストする方法、アップロード実装はMultipartFileの使い方、@WebMvcTestやMockMvcの基本はMockMvcを使ったControllerの単体テストに委譲し、本記事は受理・拒否ロジックの検証に集中します。

テスト対象とする最小限のサンプルAPI

Controllerは @RequestParam MultipartFile を受け取り、Serviceの store() に渡すだけの構成を前提にします。store() 内でサイズとMIMEタイプを検証し、違反時は InvalidFileException を投げます。テストでは @WebMvcTest と @MockitoBean で FileUploadService をモック化します( @MockBean はSpring Boot 3.4以降 @MockitoBean に統合され非推奨になったため、新規コードでは @MockitoBean を使いましょう)。

MockMultipartFileの基本的な生成方法

コンストラクタは name、originalFilename、contentType、content(byte[])の4引数です。

MockMultipartFile file = new MockMultipartFile(
    "file",        // @RequestParamの名前と一致させる
    "sample.png",   // 元のファイル名
    "image/png",     // Content-Type
    "dummy-image-content".getBytes(StandardCharsets.UTF_8)
);

name がControllerの @RequestParam 名とズレても、Spring MVCの標準エラーハンドリングによってやはり400は返ります。ただし @ControllerAdvice で定義した独自のエラーボディ形式にはならず、Springのデフォルトのエラーメッセージになるので、jsonPath() の検証が想定通りに通らず原因に気づきにくい点に注意しましょう。

空ファイル(0バイト)を試したい場合は content に null ではなく new byte[0] を渡せば表現できます。

perform(multipart())で正常系リクエストを組み立てる

multipart() に .file() でパートを積んで送信します。Serviceの戻り値は事前にスタブしておきます。

when(fileUploadService.store(any()))
    .thenReturn(new FileUploadResponse("sample.png", 123L));

mockMvc.perform(multipart("/api/files").file(file))
    .andExpect(status().isCreated())
    .andExpect(jsonPath("$.filename").value("sample.png"));

ファイルサイズ上限超過時の異常系テスト

上限を超えるバイト配列を用意し、Serviceに例外をスローさせて400を検証します。

byte[] oversized = new byte[11 * 1024 * 1024]; // 10MBの上限を超える

MockMultipartFile bigFile = new MockMultipartFile(
    "file", "big.png", "image/png", oversized
);

when(fileUploadService.store(any()))
    .thenThrow(new InvalidFileException("ファイルサイズが上限(10MB)を超えています"));

mockMvc.perform(multipart("/api/files").file(bigFile))
    .andExpect(status().isBadRequest())
    .andExpect(jsonPath("$.message").value("ファイルサイズが上限(10MB)を超えています"));

spring.servlet.multipart.max-file-size を超えた場合にServlet側で発生する MaxUploadSizeExceededException は、@WebMvcTest が使う MockMvc のマルチパート処理がServletコンテナの実際のマルチパートパーサーを経由しないため再現しにくいことがあります(詳細はSpring Bootのマルチパート設定を参照)。そのため本記事ではService内の独自バリデーションに絞ってテストするのが現実的です。

不正なMIMEタイプ・拡張子の異常系テスト

複数の不正パターンは @ParameterizedTest でまとめると読みやすくなります。

@ParameterizedTest
@ValueSource(strings = {"application/octet-stream", "text/plain", "application/zip"})
void rejectsDisallowedContentType(String contentType) throws Exception {
    MockMultipartFile invalidFile = new MockMultipartFile(
        "file", "sample.exe", contentType, "dummy".getBytes()
    );

    when(fileUploadService.store(any()))
        .thenThrow(new InvalidFileException("許可されていないファイル形式です"));

    mockMvc.perform(multipart("/api/files").file(invalidFile))
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.message").value("許可されていないファイル形式です"));
}

拡張子だけ変えてcontentTypeは正規のまま、あるいは逆のケースも同じ構造で増やせます。実際のクライアントは image/png; charset=UTF-8 のようにcharset付きで送ってきたり、大文字小文字が混在したcontentTypeを送ってくることもあります。許可判定で単純な equals() 比較をしていると正規のファイルが誤って拒否されるので、そうしたケースも @ParameterizedTest に加えておくと安心です。

例外ハンドラとテストの対応関係

テストが検証すべきなのは例外クラスではなく、クライアントに返るHTTPレスポンスです。@ControllerAdvice の実装詳細はREST APIの例外処理に委譲します。

@ExceptionHandler(InvalidFileException.class)
public ResponseEntity<ErrorResponse> handleInvalidFile(InvalidFileException e) {
    return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}

@MockitoBean でServiceに例外を投げさせれば、ハンドラの中身を意識せずステータスとボディだけで検証できます。期待と違うステータスが返ってきたときは andDo(print()) でリクエスト・レスポンスの実際の内容を確認すると原因が早く見つかります。

複数ファイル・フォームフィールド併用のテスト

複数ファイルは .file() を複数回呼び、フォームフィールドは .param() で追加します。

mockMvc.perform(multipart("/api/files/batch")
        .file(file1)
        .file(file2)
        .param("category", "document"))
    .andExpect(status().isCreated());

1つだけ不正なMIMEタイプにすれば、同じ構造で「複数ファイル中の1件だけ拒否される」ケースも検証できます。

まとめ

正常系・サイズ超過・不正MIMEタイプの3パターンを押さえれば、アップロードAPIのテストは実務上十分なカバレッジになります。MockMultipartFile でテストデータを用意し、perform(multipart()) でリクエストを組み立て、@MockitoBean で例外を発生させてレスポンスを検証する流れを覚えておけば応用できます。ダウンロード側はMockMvcでファイルダウンロードAPIをテストする方法で解説しています。