Spring BootのControllerのJUnitで使うリクエストボディを作る
動作確認環境
- Java 11
- Spring Boot 2.2
- JUnit 4.12
paramかparamsでリクエストボディを作る
multipart/form-dataで受け取るSpring BootのControllerのJUnitでは、MockHttpServletRequestBuilder
のparam
かparams
でリクエストボディで渡すパラメータを作ることができる。
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等は略
}
テストクラスは一旦MockHttpServletRequestBuilder
のparam
を使ってセットしたものを記載する。
@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");
}},
"誕生日が云々……",
"略");
// 略
}