Spring Boot 跟 @WebMvcTest 测试 MVC Web Controller(1)

让我们来回顾一下每个职责,看看我们如何使用 MockMvc 来验证每一个职责,以便构建我们力所能及的最好的集成测试。

1.验证 HTTP 请求匹配

==============

验证控制器是否侦听某个 HTTP 请求非常简单。我们只需调用 MockMvc 的 perform() 方法并提供我们要测试的 URL:

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

除了验证控制器对特定 URL 的响应之外,此测试还验证正确的 HTTP 方法(在我们的示例中为 POST)和正确的请求内容类型。我们上面看到的控制器会拒绝任何具有不同 HTTP 方法或内容类型的请求。

请注意,此测试仍然会失败,因为我们的控制器需要一些输入参数。

更多匹配 HTTP 请求的选项可以在 MockHttpServletRequestBuilder 的 Javadoc 中找到。

2.验证输入序列化

=========

为了验证输入是否成功序列化为 Java 对象,我们必须在测试请求中提供它。输入可以是请求正文的 JSON 内容 (@RequestBody)、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 和控制器期望的请求正文。请求正文是使用 Spring Boot 提供的 ObjectMapper 生成的,将 UserResource 对象序列化为 JSON 字符串。

如果测试结果为绿色,我们现在知道控制器的 register() 方法已将这些参数作为 Java 对象接收,并且它们已从 HTTP 请求中成功解析。

3.验证输入验证

========

假设 UserResource 使用 @NotNull 注释来拒绝 null 值:

@Value``public class UserResource {`

@NotNull private final String name;

@NotNull private final String email;

`}

当我们将 @Valid 注解添加到方法参数时,Bean 验证会自动触发,就像我们在控制器中使用 userResource 参数所做的那样。因此,对于快乐路径(即验证成功时),我们在上一节中创建的测试就足够了。

如果我们想测试验证是否按预期失败,我们需要添加一个测试用例,在该用例中我们将无效的 UserResource JSON 对象发送到控制器。然后我们期望控制器返回 HTTP 状态 400(错误请求):

@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);``}

我们希望控制器将传入的 UserResource 对象转换为 User 并将此对象传递给 registerUser() 方法。

为了验证这一点,我们可以要求 RegisterUseCase 模拟,它已使用 @MockBean 注解注入到应用程序上下文中:

@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”);``}

在执行了对控制器的调用之后,我们使用 ArgumentCaptor 来捕获传递给 RegisterUseCase.registerUser() 的 User 对象并断言它包含预期值。

调用 verify 检查 registerUser() 是否被调用过一次。

请注意,如果我们对 User 对象进行大量断言,我们可以 创建自己的自定义 Mockito 断言方法 以获得更好的可读性。

5.验证输出序列化

=========

调用业务逻辑后,我们希望控制器将结果映射到 JSON 字符串并将其包含在 HTTP 响应中。在我们的例子中,我们希望 HTTP 响应正文包含一个有效的 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));``}

要对响应主体进行断言,我们需要使用 andReturn() 方法将 HTTP 交互的结果存储在 MvcResult 类型的变量中。

然后我们可以从响应正文中读取 JSON 字符串,并使用 isEqualToIgnoringWhitespace() 将其与预期的字符串进行比较。我们可以使用 Spring Boot 提供的 ObjectMapper 从 Java 对象构建预期的 JSON 字符串。

请注意,我们可以通过使用自定义的 ResultMatcher 使其更具可读性,稍后对此加以描述。

6.验证异常处理

========

通常,如果发生异常,控制器应该返回某个 HTTP 状态。400 — 如果请求有问题,500 — 如果出现异常,等等。

默认情况下,Spring 会处理大多数这些情况。但是,如果我们有自定义异常处理,我们想测试它。假设我们想要返回一个结构化的 JSON 错误响应,其中包含请求中每个无效字段的字段名称和错误消息。我们会像这样创建一个 @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 数据结构来处理这个异常。在这种情况下,异常处理程序会导致所有控制器返回 HTTP 状态 400,并将 ErrorResult 对象作为 JSON 字符串放入响应正文中。

为了验证这确实发生了,我们扩展了我们之前对失败验证的测试:

@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);``}

同样,我们从响应正文中读取 JSON 字符串,并将其与预期的 JSON 字符串进行比较。此外,我们检查响应状态是否为 400。

这也可以以可读性更强的方式实现,我们接下来将要学习。

创建自定义 ResultMatcher

===================

某些断言很难写,更重要的是,很难阅读。特别是当我们想要将来自 HTTP 响应的 JSON 字符串与预期值进行比较时,它需要大量代码,正如我们在最后两个示例中看到的那样。

幸运的是,我们可以创建自定义的 ResultMatcher,我们可以在 MockMvc 的流畅 API 中使用它们。让我们看看如何做到这一点。

匹配 JSON 输出

==========

使用以下代码来验证 HTTP 响应正文是否包含某个 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 的入口点。它返回实际的 ResultMatcher,它从 HTTP 响应正文解析 JSON,并将其与传入的预期对象逐个字段进行比较。

匹配预期的验证错误

=========

我们甚至可以更进一步简化我们的异常处理测试。我们用了 4 行代码来验证 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,我们必须从上面添加方法 containsErrorMessageForField() 到我们的 ResponseBodyMatchers 类:

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(); `}``}

所有丑陋的代码都隐藏在这个辅助类中,我们可以在集成测试中愉快地编写干净的断言。

结论

==

Web 控制器有很多职责。如果我们想用有意义的测试覆盖一个 web 控制器,仅仅检查它是否返回正确的 HTTP 状态是不够的。

通过 @WebMvcTest,Spring Boot 提供了我们构建 Web 控制器测试所需的一切,但为了使测试有意义,我们需要记住涵盖所有职责。否则,我们可能会在运行时遇到丑陋的惊喜。

小编为大家准备一下spring boot的资料

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
5599294788)]

[外链图片转存中…(img-owytba8D-1715599294788)]

[外链图片转存中…(img-SVs9n3qP-1715599294789)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值