关于spring:用WebMvcTest测试MVC-Web-Contorller二

41次阅读

共计 8035 个字符,预计需要花费 21 分钟才能阅读完成。

原文 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):

@Test
void 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 值:

@Test
void 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):

@Test
void 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:

@Test
void 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 对象:

@Test
void 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:

@ControllerAdvice
class 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。

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

@Test
void 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 模式是不是很难受?

@Test
void 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 响应蕴含了特定的错误信息。咱们能够应用一行代替:

@Test
void 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

正文完
 0