头图
原文 https://reflectoring.io/sprin...
翻译: 祝坤荣

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

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

样例代码

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

测试Spring Boot系列

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

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

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

依赖

我们会使用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校验

然后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对象,对业务逻辑进行模拟,然后调用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。

你可以在我讲述模拟的文章来看更多关于@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的内部原理,是一本不错的学习书籍。
image.png


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

开发者/科幻爱好者/硬核主机玩家/业余翻译
微博:祝坤荣
B站: https://space.bilibili.com/23...
交流Email: zhukunrong@yeah.net


祝坤荣
1k 声望1.5k 粉丝

科幻影迷,书虫,硬核玩家,译者