用@WebMvcTest测试MVC Web Contorller(一)

原文 https://reflectoring.io/spring-boot-web-controller-test/ 

翻译: 祝坤荣

在这个测试Spring Boot系列的第二部分,我们来看下web contoller。开始,我们会探索下web controller到底做了什么,然后我们可以基于写单元测试来覆盖所有它的职责。

然后,我们来看看如果在测试用覆盖这些职责。只有当所有这些职责都被覆盖到了,我们才可以肯定我们的contoller的行为应该与线上环境一样了。

样例代码

这篇文章提供在GitHub[1]上的可运行代码。

测试Spring Boot系列

这篇教程是一个系列的一部分:

1.Spring Boot的单元测试[2]2.使用@WebMvcTest测试Spring Boot的MVC Web Controller[3]3.使用@DataJpaTest测试Spring Boot的JPA Queries[4]4.使用@SpringBootTest进行集成测试[5]

如果你喜欢看视频学习,可以看看Philip的测试Spring Boot应用课程[6](如果你通过这个链接购买,我有分成)。

依赖

我们会使用JUnit Jupiter(JUnit 5)作为测试框架,Mockito来模拟,AssertJ来建立断言,Lombok来减少冗余代码:

dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  compileOnly('org.projectlombok:lombok')
  testCompile('org.springframework.boot:spring-boot-starter-test')
  testCompile 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
  testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
}

AssertJ和Mockito会通过引入spring-boot-starter-test自动引入。

Web Controller的职责

让我们先看一个典型的REST controller:

@RequiredArgsConstructor
class RegisterRestController {
  private final RegisterUseCase registerUseCase;


  @PostMapping("/forums/{forumId}/register")
  UserResource register(
          @PathVariable("forumId") Long forumId,
          @Valid @RequestBody UserResource userResource,
          @RequestParam("sendWelcomeMail") boolean sendWelcomeMail) {


    User user = new User(
            userResource.getName(),
            userResource.getEmail());
    Long userId = registerUseCase.registerUser(user, sendWelcomeMail);


    return new UserResource(
            userId,
            user.getName(),
            user.getEmail());
  }


}

Controller的方法通过@PostMapping的声明来定义需要监听的URL,HTTP方法和content类型。

它接受通过@PathVariable, @RequestBody和@RequestsParam声明的入参,其被进入的HTTP请求自动填充。

参数可能被声明成@Valid来标明Spring需要对它们进行bean校验[7]

然后controller使用这些参数,调用业务逻辑,得到一个简单Java对象,其会被以JSON的形式默认自动写入到HTTP响应body中。

这里有很多Spring魔法。简单来说,对每一个请求,controller通常经过以下步骤:

# 职责 描述
1. 监听HTTP请求 controller需要对特定的URL,HTTP方法和content类型做响应
2. 反序列化输入 controller需要解析进入的HTTP请求并从URL,HTTP请求参数和请求body中创建Java对象,这样我们在代码中使用
3. 检查输入 controller是防御不合法输入的第一道防线,所以这是个校验输入的好地方
4. 调用业务逻辑 得到了解析过的入参,controller需要将入参传给业务逻辑期望的业务模型
5. 序列化输出 controller得到业务逻辑的输出并将其序列化到HTTP响应中
6. 翻译异常 如果某些地方有异常发生了,controller需要将其翻译成一个合理的错误消息和HTTP状态码

所以controller有一大堆活要干! 我们要注意不要再填加更多的像执行业务逻辑这样的职责了。那样的话,我们的controller测试会过于冗余并难以维护。

我们如果编写可以覆盖所有这些职责的合理测试呢?

单元或集成测试?

我们是写单元测试?还是写集成测试呢?这两个有什么不同?让我们看看两种方式并选择其中一个。

在单元测试中,我们需要将controller隔离测试。这表示我们要初始化一个controller对象,对业务逻辑进行模拟[8],然后调用controller的方法并校验返回。

这在我们的例子里行吗?让我们看下在上面我们定义的6个职责能否在隔离的单元测试中覆盖:

# 职责 描述
1. 监听HTTP请求 不行,因为单元测试不会检查@PostMapping声明并模拟HTTP请求的特定参数
2. 反序列化输入 不行,因为像@RequestParam和@pathVariable这样的声明不会被检验。我们会以Java对象的形式提供输入,这会跳过HTTP请求的反序列化
3. 检查输入 不行,不依赖bean校验,因为@Valid声明不会被校验。
4. 调用业务逻辑 可以,因为我们可以校验业务逻辑被期望的参数调用
5. 序列化输出 不行,因为只能校验Java版本的输出,HTTP返回不会生成
6. 翻译异常 不行,我们可以检查一个特定的异常是否产生,但它不会被翻译成一个JSON返回或HTTP状态码

简单来说,一个简单的单元测试不能覆盖HTTP层。所以我们要将Spring引入到测试中来帮我们做点HTTP魔法。因此,我们构建一个集成测试来测试我们controller代码与Spring提供的HTTP支持组件的集成。

一个Spring集成测试启动一个Spring包含所有我们需要bean的应用上下文。这包括了负责监听特定URL,序列化与反序列化JSON并翻译HTTP异常的框架bean。这些bean会检查在一个简单的单元测试里会被忽略的声明。

那么,我们怎么做呢?

使用@WebMvcTest校验Controller的职责

Spring Boot提供了@WebMvcTest声明来加载只包括了需要测试web controller的bean的应用上下文:

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = RegisterRestController.class)
class RegisterRestControllerTest {
  @Autowired
  private MockMvc mockMvc;


  @Autowired
  private ObjectMapper objectMapper;


  @MockBean
  private RegisterUseCase registerUseCase;


  @Test
  void whenValidInput_thenReturns200() throws Exception {
    mockMvc.perform(...);
  }


}
@ExtendWith


这篇教程的代码样例使用了@ExtendWith声明来告诉JUnit 5来开启Spring支持。在Spring Boot 2.1,我们不再需要加载SpringExtension了,因为它已经被包含在像@DataJpaTest,@WebMvcTest和@SpringBootTest这样的Spring Boot测试声明中了。

现在我们可以在所有我们在应用上下文中需要的bean上使用@Autowire了。Spring Boot会自动提供像@ObjectMapping这样的bean来做映射并从JSON和MockMvc实例来模拟HTTP请求。

我们使用@MockBean来模拟业务逻辑,因为我们并不想测试controller与业务逻辑的集成,而只是要测试controller与Http层的集成。@MockBean自动用Mockito的mock来替换应用上下文与被替换的bean同类型的bean。

你可以在我讲述模拟的文章[9]来看更多关于@MockBean的内容。

使用@WebMvcTest


在上例中通过将controller的参数设置到RegisterRestController.class上,我们告诉Spring Boot在创建上下文时限制给定的controller和一些Spring Web MVC框架的bean。而其他我们可能需要的bean被@MockBean隔离或模拟掉了。


如果我们不传controllers参数,Spring Boot会加载应用上下文中的所有controller。这样我们就需要加载或模拟每个controller依赖的所有bean。这回事测试的配置变得复杂的多,但由于所有的controller测试都可以重用相同的应用上下线而节省了时间。


我打算将应用上下文缩小来限制controller测试,这样可以让测试保持独立,不需要引入其他的bean,尽管这样会让Spring Boot在每次单个测试时都会建一个新的应用上下文。


让我们看一下每个职责,并看看如果通过使用MockMvc来校验每项职责来进行最佳的集成测试。


插入一条推荐内容,我与其他2位作者一起翻译的Spring 5设计模式21年2月已经在京东等电商渠道上架了,本书主要讨论了在Spring框架中使用的经典设计模式,能帮助开发者了解Spring的内部原理,是一本不错的学习书籍。

用@WebMvcTest测试MVC Web Contorller(一)_第1张图片


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

开发者/科幻爱好者/硬核主机玩家/业余翻译 微博:祝坤荣 B站: https://space.bilibili.com/23185593/ 交流Email: [email protected][10]

References

[1] GitHub: https://github.com/thombergs/code-examples/tree/master/spring-boot/spring-boot-testing
[2] Spring Boot的单元测试: https://reflectoring.io/unit-testing-spring-boot/
[3] 使用@WebMvcTest测试Spring Boot的MVC Web Controller: https://reflectoring.io/spring-boot-web-controller-test/
[4] 使用@DataJpaTest测试Spring Boot的JPA Queries: https://reflectoring.io/spring-boot-data-jpa-test/
[5] 使用@SpringBootTest进行集成测试: https://reflectoring.io/spring-boot-test/
[6] 测试Spring Boot应用课程: https://transactions.sendowl.com/stores/13745/194393
[7] bean校验: https://reflectoring.io/bean-validation-with-spring-boot/
[8] 业务逻辑进行模拟: https://reflectoring.io/unit-testing-spring-boot/#using-mockito-to-mock-dependencies
[9] 述模拟的文章: https://reflectoring.io/spring-boot-mock/
[10] [email protected]mailto:[email protected]

你可能感兴趣的:(设计模式,java,spring,spring,boot,python)