相关文章推荐
成熟的匕首  ·  SpringMvc ...·  1 周前    · 
性感的西装  ·  Spring ...·  1 周前    · 
俊秀的大葱  ·  spring boot 3.0.6 ...·  1 周前    · 
温暖的键盘  ·  spring data ...·  1 周前    · 
慷慨的烤面包  ·  android ...·  1 年前    · 
慷慨大方的风衣  ·  Excel 自動補0 - iT ...·  1 年前    · 
SpringBoot Test 人类使用指南

SpringBoot Test 人类使用指南

2 年前

测试好处多多。但在 spring boot 里写测试,别说得到好处,就连把测试框架搭对都不是个简单的事。

毋庸置疑, 相对于 Golang, python 的网络测试框架, java 里 spring 里的测试框架真是复杂的可以. 约定优于配置, 这约定漫天飞舞藏在文档的各个角落. 版本还不统一.


一般我们写后端逻辑分 3 层, Controller -> Service -> Repository,简单来说,

对于 单元测试 ,我们只针对某一个功能点写测试

而对于 集成测试 ,我们会集成多个功能。

spring boot 为这两种测试,都提供了具体的约定方法。

spring boot 启动慢,我们测试时应该尽可能的只启动我们需要的类。 怎么做到呢?

spring boot 分层测试

我们看上面 spring 简化的请求图。 如果是单元测试。我们应该只对 3 测(依赖 4),只对 4 测(依赖 5)。 而集成测试,我们应该是可以测 1 到 5. 当然,这个界定根据你的需求来。

在测试前,我们先把 pom 的包统一下.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
    <relativePath/>
</parent>
<dependencies>
   <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

完整源码地址: github.com/zk4/spring_t

测试场景

我们要将测试场景列个矩阵

不加载 spring 框架 的单元测试

这是最灵活也是最快的一种方案

拿 Controller 举例

Controller 其实就是一个类. 理论上 new 出来测就行了

但问题是: Controller 里会被 Spring 注入一堆东西.

简单的解决方法是:

不要在成员上加 @Autowired, 而是在构造时自动注入. 这也是 Spring 官方的推荐做法.

@Controller
class  UserController{
  //不要这样 
  @Autowired
  UserService userService;  
@Controller
class  UserController{
  UserService userService;
  //建议这样
  @Autowired
  UserController(UserService userService){
    this.userService = userService;
}


那 UserService 直接 new? UserService 也是有 Spring 注入的.

我们知道 java 里可以给类做代理.

那么,对构造函数是类的, 我们只要代理这个类,然后模拟类函数的返回值就行了.

这个过程. spring test 里的 Mokito 做了封装.

在 Test 类里, 操作如下:

//测试类的头, 使 Mock 生效
@RunWith(MockitoJUnitRunner.class)
public class UserControllerTest{
  // 指定要 Mock 的类
  @Mock
  UserService userService;  
  @Test
  public testhello(){
      // Mock 的类里的函数怎么返回
      given(userService.getUser(1))
          .willReturn(new User().setName("bob").setId(1));
}


怎么模拟 http 请求访问呢?

Mockito 帮你做, 但不是真正的网络请求! 是模拟的. 就是不会经过网卡. 而是直接将模拟网络请求塞给 Controller.

见下面 @Test 方法.

package com.zk.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zk.entity.User;
import com.zk.exception.UserNotFound;
import com.zk.service.UserService;
import org.assertj.core.api.Assertions;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
//由 Junit 4 启动 Mockito
@RunWith(MockitoJUnitRunner.class)
@AutoConfigureRestDocs
public class MyControllerTest1 {
  private MockMvc mvc;
  @Mock
  // 要 Mock 的类
  UserService  userService;
  @InjectMocks
  // Mock 要注入的类
  UserController userController;
  @Before
  public void setUp()   {
    mvc = MockMvcBuilders.standaloneSetup(userController)
        //指定 Exception 处理器
        .setControllerAdvice(new UserExceptionAdvice())
        //.addFilters(new UserFilter())  //你也可以指定 filter , interceptor 之类的, 看 StandaloneMockMvcBuilder 源码
        .build();
  @Test
  public void getUserTest() throws Exception {
    // given
    User bob = new User().setName("bob").setId(1);
    given(userService.getUser(1))
        .willReturn(bob);
    //  when
    MockHttpServletResponse response = mvc.perform(
        get("/user/1")
            .accept(MediaType.APPLICATION_JSON)
        .andReturn()
        .getResponse();
    // then
    ObjectMapper objectMapper = new ObjectMapper();
    Assert.assertEquals(response.getStatus(), HttpStatus.OK.value());
    Assert.assertEquals(response.getContentAsString(), objectMapper.writeValueAsString(bob));
  @Test
  public void getUserNotFound() throws Exception {
     //given
    given(userService.getUser(999))
        .willThrow(new UserNotFound());
    // when
    MockHttpServletResponse response = mvc.perform(
        get("/user/999")
            .accept(MediaType.APPLICATION_JSON))
        .andReturn().getResponse();
    // then
    Assertions.assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
    Assertions.assertThat(response.getContentAsString()).isEmpty();


加载 spring 框架, 集成测试

这个相对来说最简单的. 也是最符合直觉的,

但就是, 太慢. 要启整个 Spring.

package com.zk.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zk.entity.User;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyControllerTest2 {
  // 要注意, 不要用 RestTemplate,
  // 因为 TestRestTemplate 在测试环境里多做了很多事,
  // 比如: 帮你自己把当前 host:port 加上了. (尤其是咱们还指定了随机端口)
  //      能自动加账号密码,
  //      ErrorHandler 被设成了 NoOpResponseErrorHandler.
  //      最重要的, 能在测试类里一键注入啊...  
  @Autowired
  TestRestTemplate restTemplate;
  @Test
  public void getUser() throws Exception {
    // given
    User user = new User().setName("bob").setId(1);
    // when
    ResponseEntity<User> response = restTemplate.getForEntity("/user/1", User.class);
    // then
    ObjectMapper objectMapper=new ObjectMapper();
    Assert.assertEquals(response.getStatusCode(),HttpStatus.OK);
    Assert.assertEquals(
        objectMapper.writeValueAsString(response.getBody()),
        objectMapper.writeValueAsString(user)


怎么 Mock?

假如我们要替换 UserService 的返回. 但 spring 的 interceptor, AdviceController 之类的都要正常加载咋整.

你可以按上一节的方式,也有更简单的方法.

在 @SpringBootTest 的加成下! 你可以直接使用 @AutoConfigureMockMvc

package com.zk.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zk.entity.User;
import com.zk.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 允许 Mock
@AutoConfigureMockMvc  
public class SpringRunner_mock {
  @Autowired
  MockMvc mvc;
  @MockBean
  UserService userService;
  @Test
  public void getUser() throws Exception {
    // given
    User bob = new User().setName("bob").setId(1);
    given(userService.getUser(1))
        .willReturn(bob);
    // when
    MockHttpServletResponse response = mvc.perform(
        get("/users/1")
            .accept(MediaType.APPLICATION_JSON))
        .andReturn().getResponse();
    // then
    ObjectMapper objectMapper=new ObjectMapper();
    assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
    assertThat(response.getContentAsString()).isEqualTo(
        objectMapper.writeValueAsString(bob)


加载 spring 部分框架(仅自动加载 Controller) 单元测试

通过 @WebMvcTest 指定即可. 你也可以不写指定的 Controller.class ,那你得 Mock 所有 Controller 的依赖才行.

@WebMvcTest 这个注解干的事就多了... 你可以点开源码看一眼, 有没有熟悉的 @AutoConfigureMockMvc

package com.zk.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zk.entity.User;
import com.zk.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class SpringRunner_unit_controller_only {
  @Autowired
  private MockMvc mvc;
// 会自动注入到 controller
  @MockBean
  private UserService userService;
  @Test
  public void getUser() throws Exception {
    // given
    User bob = new User().setName("bob").setId(1);
    given(userService.getUser(1))
        .willReturn(bob);
    // when
    MockHttpServletResponse response = mvc.perform(
        get("/users/1")
            .accept(MediaType.APPLICATION_JSON))
        .andReturn().getResponse();
    // then
    ObjectMapper  objectMapper = new ObjectMapper();