概述

在传统的Springboot项目中,当你按照接口文档写完API后,会手写测试用例对接口进行测试,包括接口是否是连通的,边界值等, 这是我们在完成任务时对代码质量的保证。而在线上部署时,要求我们在部署前去做接口的最后一次测试,以便于在maven/gradle build项目时,能够安全通过最后一道关卡。

顺带一提,我们自己使用的是CI/CD 去构建项目的。

步骤

请确定你自己的版本 , 这很重要,不同版本的实现细节有区别

<groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <version>2.4.2</version>

在此版本下 (不说版本的都是耍流氓~)

//标准Springboot测试需要加的注解,classes指定一个Springboot启动类
@SpringBootTest(classes = UserApplication.class)
//自动注入MockMvc
@AutoConfigureMockMvc
class UserControllerTest{
@Autowired
private MockMvc mockMvc;

...
}

这里MockMvc 也有别的注入方式, 可以通过下列,

@Autowired
    private WebApplicationContext webApplicationContext;
    private MockMvc mockMvc;
// ++++
    @BeforeEach
    public void setUp() throws Exception{
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();//建议使用这种
    }

注: 有些可能会使用@Bofore 不生效, 还是去看junit的版本,官网有说:某些版本以后要使用@BeforeEach替换@Before

在上述完成以后,就能正常去写我们的测试用例了。

@Test
    void test() throws Exception {
        MvcResult mvcResult = mockMvc.perform(
                MockMvcRequestBuilders.get("/api/v1/usr/locked")
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.parseMediaType("application/json;charset=UTF-8"))
                        .param("username" , "xxx"))
                //期望结果状态为200
                .andExpect(MockMvcResultMatchers.status().isOk())
                //需要打印到控制台
                .andDo(MockMvcResultHandlers.print())
                //返回结果
                .andReturn();
    }

使用已经注入好的mockMvc发起调用,这里可以使用MockMvcRequestBuilders.get/post/delete/put rest风格的方式去调用,因为默认调用的是本身,所以只需要些URI,和我们使用Controller是一样的,可以参照@RequestMapping去写。这里传递参数有下列几种方式

//1 post
		UserVo userVo = new UserVo();
        userVo.setUsername("xx");
        userVo.setPassword("xx");
        JSONObject jsonObject = new JSONObject(userVo);
        String jsonStr = jsonObject.toString();
        
  MockMvcRequestBuilders.post("/api/v1/usr/locked")
                        .content(jsonStr)

//2 get(1)
   MockMvcRequestBuilders.get("/api/v1/usr/locked")
                         .param("username" , "xxx"))
//3 get(2) 直接拼接uri传递参数

执行结果:

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /api/v1/usr/password
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Accept:"application/json;charset=UTF-8", Content-Length:"41"]
             Body = {"password":"xxx","username":"xx"}
    Session Attrs = {}

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json;charset=UTF-8"]
     Content type = application/json;charset=UTF-8
             Body = {"status":0,"message":"success","data":xxxxxxxxxxxxxxxxx}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

会看到一系列的结果,可以使用Assert断言去判断想要的结果;

其实到这里,我们只需要把每个测试用例的入参、uri 等按照请求规则填写好,那么在正常build过程,就可以走过所有的测试方法,实现maven/gradle build 在构建时,提前对接口进行测试。

问题描述

但是由于存在测试修改数据的风险,比如增删改的操作,某些情况是不能使用这样的方法测试的。比如你要删除某个用户,你的测试用例也只能够用一次,遇到这样的情况,如果直接修改库,看起来会很笨拙,测试也增大了开发的负担。

我们可以用到 @MockBean 注解。这个注解会生成相应类的代理类,代理后在执行测试时,是不会走实际的被代理类的方法,而是走代理类;这样在核心的业务这里就不会产生脏数据。

有如下几个维度去使用@MockBean:

  1. Controller
@Autowired
    private MockMvc mockMvc;

	//在Controller层标记MockBean
    @MockBean
    private UserController userController;

只会测试到接口是否正常,body始终都是空的,因为并没有产生对真实Controller的调用,所以没有返回结果

MockHttpServletResponse:
Status = 200
Error message = null
Headers = []
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []

  1. Service
@Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

其他结果不变,会输出(如果有真实结果也无法调用到具体的Service方法,Service断点没有拦截到)

Body = {“status”:0,“message”:“success”,“data”:null}

没有调用到Service,只是简单测试了接口的连通性,当然如果你的测试用例有问题,照样是无法通过的

mvn install

[INFO] xxx-user-api … FAILURE [ 8.245 s]

  1. Mapper
@MockBean
    private UserMapper userMapper;

如果是增删改的操作,使用mapper层会调用到Service层的业务逻辑, 但是userMapper是调用不到的,这里使用MockBean生成的代理去替换了userMapper
如果其他方面没有影响,使用到数据库/其他可能修改到的数据结构 前的一层测试是最完美的。
eg: redisTemplate

结论

至此 ,Springboot的接口测试可以分为两部分

  • 查询操作,可以构建测试用例,对接口进行测试,可以通过andExpect的方式添加期望结果,进行接口的简单测试或者是加入断言对结果进行判断;
  • 影响最终结果的测试,那么在可能会影响数据结果前使用@MockBean注入,那么既不会对结果产生影响,又能测试对应接口该有的功能。