Spring BootのControllerのJUnitで使うリクエストボディを作る

動作確認環境

  • Java 11
  • Spring Boot 2.2
  • JUnit 4.12

paramかparamsでリクエストボディを作る

multipart/form-dataで受け取るSpring BootのControllerのJUnitでは、MockHttpServletRequestBuilderparamparamsでリクエストボディで渡すパラメータを作ることができる。

param(String name, String... values)の引数がparams(MultiValueMap<String,String> params)の引数よりシンプルなこともあって、paramを使うことが多かった。

しかし、各パラメータをしっかりバリデーションしている場合、JUnitではさまざまな値をセットしたリクエストボディを作る必要があり、paramでは使い勝手が悪かった。

ControllerとControllerTestの実装例

param, paramsの使い方に入る前に、Controllerとそのテストクラスの実装例を書く。

Controllerとリクエストボディから見える特徴としては、multipart/form-dataで受け取り、画像ファイルと入力文字等を大量に受け取るAPIである。

@lombok.RequiredArgsConstructor
@RestController
@Validated
public class XxxController {

    private final XxxService xxxService;

    @PutMapping(value = "/api/xxx", produces = MediaType.APPLICATION_JSON_VALUE)
    String xxx(@ModelAttribute("form") @Validated XxxRequest request) {
        xxxService.xxx(request);
        return "{}";
    }

}

リクエストボディ

public class XxxRequest {
    @NotNull
    @Pattern(/*略*/)
    String name;

    @Pattern(/*略*/)
    String birthday;

    // 略

    MultipartFile portrait;

    // getter, setter等は略
}

テストクラスは一旦MockHttpServletRequestBuilderparamを使ってセットしたものを記載する。

@RunWith(SpringRunner.class)
@SpringBootTest
public class XxxControllerTest {

    @Autowired
    WebApplicationContext wac;

    MockMvc mvc;

    @Before
    public void setup() {
        mvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    public void xxxOk() throws Exception {
        mvc.perform(multipart("/api/xxx")
                        .param("name", "ナナシ")
                        .param("birthday", "2000-01-01")
                        // 略
                        .param("gender", "MALE")
                        .file(new MockMultipartFile("portrait", "some file name", "image/png", Files.readAllBytes(Paths.get("some path")))
                        .with(req -> {
                            req.setMethod(HttpMethod.PUT.name());
                            return req;
                        }))
           .andExpect(status().isOk());
    }
}

param

先ほど記載した例だと、paramで特に不便は感じない。ただ、BeanValidationによってNGになるパターンをテストするために、paramの一部を書き換えたものを大量に用意しようとすると、正常な.param部分まで含めて何度も書かなくてはならない。

@Test
public void xxxNg() throws Exception {
    // 文字列長のチェック
    mvc.perform(multipart("/api/xxx")
                    .param("name", "あ".repeat(20)) // ここが異常
                    .param("birthday", "2000-01-01") // ここは正常
                    // 略
                    .param("gender", "MALE")
                    .file(new MockMultipartFile("portrait", "some file name", "image/png", Files.readAllBytes(Paths.get("some path")))
                    .with(req -> {
                        req.setMethod(HttpMethod.PUT.name());
                        return req;
                    }))
       .andExpect(status(). isBadRequest())
       // アサーションは略
       ;

    // 日付チェック(時系列)
    mvc.perform(multipart("/api/xxx")
                    .param("name", "ナナシ") // ここは正常
                    .param("birthday", "3000-01-01") // ここが異常
                    // 略
                    .param("gender", "MALE")
                    .file(new MockMultipartFile("portrait", "some file name", "image/png", Files.readAllBytes(Paths.get("some path")))
                    .with(req -> {
                        req.setMethod(HttpMethod.PUT.name());
                        return req;
                    }))
       .andExpect(status(). isBadRequest())
       // アサーションは略
       ;

    // 延々と続く
}

これでは書くのが面倒なだけではなく、何の項目がNGな値にセットされているのかわかりにくいというデメリットもある。

params

paramsを使って問題を解決できる。paramsの引数はMultiValueMap<String,String> paramsである。MultiValueMapはSpringが用意しているものでMap<K,List<V>>を使いやすくしたものである。要はMapに過ぎないので、パラメータのバリエーションを簡単に作れる。

まずは正常系のパラメータを生成するメソッドを用意する。

private MultiValueMap<String, String> getOkParams() {
    return new LinkedMultiValueMap<>() {{
        add("name", "ナナシ");
        add("birthday", "2000-01-01");
        addAll("someList", List.of("A", "B", "C"));
        // 略
    }};
}

次にパラメータを変更するメソッドを用意する。

private MultiValueMap<String, String> customizeParams(MultiValueMap<String, String> replaceParams) {
    var params = getOkParams();
    for (var e : replaceParams.entrySet()) {
        String key = e.getKey();
        params.remove(key);
        params.addAll(key, replaceParams.get(key));
    }
    return params;
}

mvc.performからアサーションまでを行うメソッドを用意する。

private void execXxxNg(MultiValueMap<String, String> ngParams, String... messages) throws Exception {
    mvc.perform(multipart("/api/xxx")
                    .params(customizeParams(ngParams))
                    .file(new MockMultipartFile("portrait", "some file name", "image/png", Files.readAllBytes(Paths.get("some path")))
                    .with(req -> {
                        req.setMethod(HttpMethod.PUT.name());
                        return req;
                    }))
       .andExpect(status().isBadRequest())
       .andExpect(jsonPath("$.errorMessage").value(new AssertionMatcher<String>() {
           @Override
           public void assertion(String actual) throws AssertionError {
               assertThat(Arrays.stream(actual.split("\n")).sorted().collect(toList()))
                   .isEqualTo(Arrays.stream(messages).sorted().collect(toList()));
           }
       }));
}

これを利用するとNGパターンのパラメータのセットを簡単に大量に作れるだけでなく、何の項目がNGかということが一目でわかるコードになる。

@Test
public void xxxNg() throws Exception {
    // 文字列長のチェック
    execXxxNg(new LinkedMultiValueMap<>() {{
                  add("name", "あ".repeat(20));
              }},
              "名前は20文字以内で入力してください");

    // 日付チェック(時系列)
    execXxxNg(new LinkedMultiValueMap<>() {{
                  add("birthday", "3000-01-01");
                  add("abcDay", "3000-01-01");
              }},
              "誕生日が云々……",
              "略");

    // 略
}