原文 https://reflectoring.io/sprin...
翻译: 祝坤荣
浏览大概须要10分钟

1. 校验匹配HTTP申请

验证一个controller监听一个特定的HTTP申请很间接。咱们只有调用MockMvc的perform()办法并提供要测试的URL即可:

mockMvc.perform(post("/forums/42/register")    .contentType("application/json"))    .andExpect(status().isOk());

不只是校验controller会对一个特定的申请会有响应,这个测试也能够校验HTTP办法(这个例子是POST)与申请的content type是否正确。以上controller会回绝任何用了不同HTTP办法或content type的申请。

记住这个测试依然会失败,因为咱们的controller冀望一些入参。

更多匹配HTTP申请的内容能够在Javadoc MockHttpServletRequestBuilder中看到。

2. 校验输出

为了校验入参被胜利的序列化成了Java对象,咱们须要在测试申请中提供它。输出能够是申请body(@RequestBody)里的JSON内容,一个URL中的变量(@PathVariable)或一个HTTP申请中的参数(@RequestParam):

@Testvoid whenValidInput_thenReturns200() throws Exception {  UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");     mockMvc.perform(post("/forums/{forumId}/register", 42L)        .contentType("application/json")        .param("sendWelcomeMail", "true")        .content(objectMapper.writeValueAsString(user)))        .andExpect(status().isOk());}

咱们当初提供了门路变量forumId,申请参数sendWelcomeMail和controller冀望的申请body。申请body是用Spring Boot提供的ObjectMapper生成的,将UserResource对象序列化成了一个JSON字符串。

如果测试绿了,那么咱们就晓得了controller的register()办法能够将将这些HTTP申请的参数并将其解析成为Java对象。

3. 查看输出校验

让咱们看下UserResource用@NotNull申明来回绝null值:

@Testvoid whenValidInput_thenReturns200() throws Exception {  UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");     mockMvc.perform(post("/forums/{forumId}/register", 42L)        .contentType("application/json")        .param("sendWelcomeMail", "true")        .content(objectMapper.writeValueAsString(user)))        .andExpect(status().isOk());}

当咱们为办法参数减少了@Valid的申明后Bean测验会主动触发。所以,对于走乐观门路来说(比方让测验胜利),咱们在上一节创立的测试曾经足够了。

如果咱们想要测试一下测验失败的状况,咱们须要加一个测试用例,发送一个不非法的UserResouceJSON对象给controller.咱们冀望controller返回HTTP状态400(Bad request):

@Testvoid whenNullValue_thenReturns400() throws Exception {  UserResource user = new UserResource(null, "zaphod@galaxy.net");    mockMvc.perform(post("/forums/{forumId}/register", 42L)      ...      .content(objectMapper.writeValueAsString(user)))      .andExpect(status().isBadRequest());}

取决于这个校验对于利用有多重要,咱们能够为每个不非法的值加一个测试用例。这样能够疾速增加大量测试用例,所以你须要与团队阐明下你到底想要如何在你的我的项目里解决校验的测试。

4. 校验业务逻辑调用

上面,咱们想要校验一下业务逻辑的调用是否合乎预期。在这个例子,业务逻辑是由RegisterUseCase接口提供的并冀望以一个User对象和一个boolean作为输出:

interface RegisterUseCase {  Long registerUser(User user, boolean sendWelcomeMail);}

咱们冀望controller将传入的UserResource对象转成User并将这个对象传给registerUser()办法。

为了验证这个,咱们能够模仿RegisterUseCase,其是被申明了@MockBean申明并被注入到application context:

@Testvoid whenValidInput_thenMapsToBusinessModel() throws Exception {  UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");  mockMvc.perform(...);  ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);  verify(registerUseCase, times(1)).registerUser(userCaptor.capture(), eq(true));  assertThat(userCaptor.getValue().getName()).isEqualTo("Zaphod");  assertThat(userCaptor.getValue().getEmail()).isEqualTo("zaphod@galaxy.net");}

当调用了controller后,咱们应用ArgumentCaptor来捕获传给RegisterUseCase.registerUser()的User对象并查看它蕴含了冀望的值。

verify用来查看registerUser()的确被调用了一次。

记住如果咱们对User对象做了很多断言假如,为了更易读,咱们能够写一个自定义Mockito断言办法。

5. 查看输入序列化

在业务逻辑被调用后,咱们冀望controller将后果封装到JSON字符串并放在HTTP响应里。在这个例子,咱们冀望HTTP响应body里有一个无效的JSON格局的UserResource对象:

@Testvoid whenValidInput_thenReturnsUserResource() throws Exception {  MvcResult mvcResult = mockMvc.perform(...)      ...      .andReturn();  UserResource expectedResponseBody = ...;  String actualResponseBody = mvcResult.getResponse().getContentAsString();    assertThat(actualResponseBody).isEqualToIgnoringWhitespace(              objectMapper.writeValueAsString(expectedResponseBody));}

为了对响应body做断言,咱们须要将HTTP交互的后果存储在一个应用andReturn办法返回的类型MvcResult中。

而后能够从响应body中读取JSON字符串并应用isEqualToIgnoringWhitespce()来比拟预期字符串。咱们能够用Spring Boot提供的ObjectMapper来将Java对象编程一个JSON字符串。

记住咱们通过应用一个自定义的ResultMatcher来让这些更易读,前面会介绍

6. 校验异样解决

通常,如果一个异样产生,controller会返回一个特定的HTTP状态码,400,是申请出问题了,500,是异样呈现了,等等。

Spring默认能解决大部分这些状况。尽管如此,如果咱们有自定义的异样解决,咱们会须要测试。比方咱们想要对每个有效的表单项返回一个结构化的带表单名和错误信息的响应。先写一个@ControllerAdvice:

@ControllerAdviceclass ControllerExceptionHandler {  @ResponseStatus(HttpStatus.BAD_REQUEST)  @ExceptionHandler(MethodArgumentNotValidException.class)  @ResponseBody  ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {    ErrorResult errorResult = new ErrorResult();    for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {      errorResult.getFieldErrors()              .add(new FieldValidationError(fieldError.getField(),                   fieldError.getDefaultMessage()));    }    return errorResult;  }  @Getter  @NoArgsConstructor  static class ErrorResult {    private final List<FieldValidationError> fieldErrors = new ArrayList<>();    ErrorResult(String field, String message){      this.fieldErrors.add(new FieldValidationError(field, message));    }  }  @Getter  @AllArgsConstructor  static class FieldValidationError {    private String field;    private String message;  }  }

如果bean校验失败,Spring抛出MethodArgumentNotValidException。咱们通过将Spring的FieldError映射到咱们本人的ErrorResult数据结构来解决这个异样。异样解决会让所有controller返回HTTP 400状态并将ErrorResult对象转成JSON字符串放在响应body。

要校验这个动作,咱们应用之前的测试来让校验失败:

@Testvoid whenNullValue_thenReturns400AndErrorResult() throws Exception {  UserResource user = new UserResource(null, "zaphod@galaxy.net");  MvcResult mvcResult = mockMvc.perform(...)          .contentType("application/json")          .param("sendWelcomeMail", "true")          .content(objectMapper.writeValueAsString(user)))          .andExpect(status().isBadRequest())          .andReturn();  ErrorResult expectedErrorResponse = new ErrorResult("name", "must not be null");  String actualResponseBody =       mvcResult.getResponse().getContentAsString();  String expectedResponseBody =       objectMapper.writeValueAsString(expectedErrorResponse);  assertThat(actualResponseBody)      .isEqualToIgnoringWhitespace(expectedResponseBody);}

一样的,咱们从响应body读取JSON字符串并与冀望的JSON字符串来比拟。而且,咱们也查看响应码是400.

这些,也能够按更可读的形式来实现,就像之前学过的

编写自定义ResultMatchers

特定的断言不太好写,更重要的是,比拟难读。特地是当咱们想要从HTTP响应中比拟JSON字符串是否合乎预期时须要写很多代码,就像咱们在上两个例子看到的。

侥幸的是,咱们能够应用MockMvc内置的API来写一个自定义的ResultMatcher。来看下在这个例子里咱们怎么做。

匹配JSON输入

如果像上面的代码一样来比拟HTTP响应body中是否蕴含一个Java对象的JSON模式是不是很难受?

@Testvoid whenValidInput_thenReturnsUserResource_withFluentApi() throws Exception {  UserResource user = ...;  UserResource expected = ...;  mockMvc.perform(...)      ...      .andExpect(responseBody().containsObjectAsJson(expected, UserResource.class));}

不须要手动比拟JSON字符串了。并且更具备可读性。事实上,代码能够自解释。

要像下面这样应用代码,咱们要写一个自定义的ResultMatcher:

public class ResponseBodyMatchers {  private ObjectMapper objectMapper = new ObjectMapper();  public <T> ResultMatcher containsObjectAsJson(      Object expectedObject,       Class<T> targetClass) {    return mvcResult -> {      String json = mvcResult.getResponse().getContentAsString();      T actualObject = objectMapper.readValue(json, targetClass);      assertThat(actualObject).isEqualToComparingFieldByField(expectedObject);    };  }    static ResponseBodyMatchers responseBody(){    return new ResponseBodyMatchers();  }  }

静态方法responseBody()作为咱们API的入口。它返回从HTTP响应body的理论ResultMatcher并且逐项比拟是否与预期对象相符。

匹配冀望的校验谬误

咱们能够进一步简化咱们的异样解决测试。这里用了四行代码来查看JSON响应蕴含了特定的错误信息。咱们能够应用一行代替:

@Testvoid whenNullValue_thenReturns400AndErrorResult_withFluentApi() throws Exception {  UserResource user = new UserResource(null, "zaphod@galaxy.net");  mockMvc.perform(...)      ...      .content(objectMapper.writeValueAsString(user)))      .andExpect(status().isBadRequest())      .andExpect(responseBody().containsError("name", "must not be null"));}

同样,代码能够自解释。

要开启这个API,咱们要下面代码里的ResponseBodyMatchers类填加containsErrorMessageForField():

public class ResponseBodyMatchers {  private ObjectMapper objectMapper = new ObjectMapper();  public ResultMatcher containsError(        String expectedFieldName,         String expectedMessage) {    return mvcResult -> {      String json = mvcResult.getResponse().getContentAsString();      ErrorResult errorResult = objectMapper.readValue(json, ErrorResult.class);      List<FieldValidationError> fieldErrors = errorResult.getFieldErrors().stream()              .filter(fieldError -> fieldError.getField().equals(expectedFieldName))              .filter(fieldError -> fieldError.getMessage().equals(expectedMessage))              .collect(Collectors.toList());      assertThat(fieldErrors)              .hasSize(1)              .withFailMessage("expecting exactly 1 error message"                         + "with field name '%s' and message '%s'",                      expectedFieldName,                      expectedMessage);    };  }  static ResponseBodyMatchers responseBody() {    return new ResponseBodyMatchers();  }}

所有的蹩脚代码都暗藏在了helper类里,而咱们能够欢快的在集成测试里编写洁净的断言代码。

论断

Web controller有许多职责。如果咱们想要用有意义的测试来笼罩一个web controller,只是查看是否返回HTTP状态码是不够的。

通过@WebMvcTest,Spring Boot提供了所有须要在web controller测试须要的货色,但要让测试有意义,咱们要记得笼罩所有职责。不然,咱们可能在利用运行时呈现惊吓。

这篇文章的代码在github上可用。


本文来自祝坤荣(时序)的微信公众号「麦芽面包,id「darkjune_think」

开发者/科幻爱好者/硬核主机玩家/业余翻译~~~~
微博:祝坤荣
B站: https://space.bilibili.com/23...
转载请注明。

交换Email: zhukunrong@yeah.net