集成测试
集成测试是指项目代码在单元测试完成后进行的第二阶段测试。集成测试的重点是在集成组件或单元之间交互时暴露缺陷,以保证不同模块之间相互调用的正确性。在Spring Boot的项目集成测试中,将测试Controller和Dao的完整请求处理。应用程序在服务器中运行,以创建应用程序上下文和所有的Bean,其中有些Bean可能会被覆盖以模拟某些行为。进行集成测试,是为了保证项目能够达到下面的目的:
降低项目中发生错误的风险;
验证接口的功能是否符合设计的初衷;
测试接口是否可用;
查找项目中的Bug并进行修复。
在项目开发中,需要开展集成测试的情况有以下4种:
单个模块由开发人员设计,而多个模块之间的设计可能不尽相同;
检查多个模块与数据库是否正确连通;
验证不同模块之间是否存在不兼容或错误的情况;
验证不同模块之间的异常和错误处理。
集成测试自动配置
Spring Boot框架支持集成测试,项目开发时只需要做简单的配置就可以完成集成测试。本节将在7.1节的基础上介绍项目配置后进行集成测试的过程。
首先在pom.xml中添加项目依赖:
<parent>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>junit5-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>junit5-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--加入JUnit5进行测试;如果想用JUnit4进行测试,则把exclusions去除-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.11.2</version>
</dependency>
</dependencies>@SpringBootTest是Spring Boot提供的一个注解,表示当前类是SpringBoot环境中的一个测试类。如果使用的是JUnit 4,则需要用到@RunWith(SpringRunner.class)和@Spring-BootTest注解。但是在JUnit 5中,只需要使用@SpringBootTest注解就可以了。
测试Spring MVC入口
下面使用Spring中的集成测试模块来测试项目入口Controller的方法。
(1)新建GoodsController类,并新建要测试的业务代码,代码如下:
package com.example.junit5demo.controller;
import com.example.junit5demo.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
@GetMapping("/queryGood")
public String queryGood(@RequestParam("name") String name) {
goodsService.queryGood(name);
return "queryGood " + name;
}
@PostMapping("/countGood")
public String countGood(@RequestBody Goods goods) {
goodsService.countGood(goods.getName());
return "countGood " + goods;
}
}
(2)新建Goods的实体类,代码如下:
package com.example.junit5demo.controller;
import lombok.Data;
@Data
public class Goods {
private long id;
private String name;
private int status;
}
(3)新建Goods的service和实现类,代码如下:
package com.example.junit5demo.service;
public interface GoodsService {
void queryGood(String name);
void countGood(String name);
}
Goods的实现类代码如下:
package com.example.junit5demo.service;
import org.springframework.stereotype.Service;
@Service
public class GoodsServiceImpl implements GoodsService {
@Override public void queryGood(String name) {
System.out.println("执行了goods的queryGood方法,参数:" + name);
}
@Override
public void countGood(String name) {
System.out.println("执行了goods的countGood方法,参数:" + name);
}
}
(4)编写Controller的集成测试用例,代码如下:
package com.example.junit5demo.controller;
import lombok.extern.SLF4J.SLF4J;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.Auto
ConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import
org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import
org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import
org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@Slf4j
@SpringBootTest
@AutoConfigureMockMvc
class GoodsControllerTest {
private MockHttpSession session; @Autowired
private MockMvc mvc;
@BeforeEach
public void setupMockMvc() {
//设置MVC
session = new MockHttpSession();
}
@Test
void queryGood() throws Exception {
MvcResult mvcResult = (MvcResult) mvc.perform(MockMvcRequest
Builders.get("/queryGood")
.accept(MediaType.ALL)
.session(session)
.param("name", "cc")
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
//得到返回代码
int status = mvcResult.getResponse().getStatus();
//得到返回结果
String result = mvcResult.getResponse().getContentAsString();
log.info("status是:{},内容是:{}", status, result);
}
@Test
void countGood() throws Exception {
String body = "{\"id\":1,\"name\":\"cc\",\"status\":2}";
MvcResult mvcResult = (MvcResult) mvc.perform(MockMvcRequest
Builders.post("/countGood")
.accept(MediaType.ALL)
.session(session)
.content(body)
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn(); //得到返回代码
int status = mvcResult.getResponse().getStatus();
//得到返回结果
String result = mvcResult.getResponse().getContentAsString();
log.info("status是:{},内容是:{}", status, result);
}
}
(5)运行第一个测试用例,在控制台上可以看到测试用例的输出结果,它执行了queryGood的GET方法,实际调用了代码中的方法,且返回了预期的结果。
执行了goods的queryGood方法,参数:cc
MockHttpServletRequest:
HTTP Method = GET
Request URI = /queryGood
Parameters = {name=[cc]}
Headers = [Accept:"*/*"]
Body = null
Session Attrs = {}
Handler:
Type = com.example.junit5demo.controller.GoodsController
Method = com.example.junit5demo.controller.GoodsController
#queryGood(String)
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:"text/plain;charset=UTF-8", Content
Length:"12"]
Content type = text/plain;charset=UTF-8
Body = queryGood cc
Forwarded URL = null
Redirected URL = null
Cookies = []
2021-07-10 15:51:30.786 INFO 15428 --- [ main]
c.e.j.controller.
GoodsControllerTest : status是:200,内容是:queryGood cc
2021-07-10 15:51:30.808 INFO 15428 --- [extShutdownHook]
o.s.s.concurrent.
ThreadPoolTaskExecutor : Shutting down ExecutorService 'application
TaskExecutor'
(6)执行第二个测试用例的方法,并执行countGood的POST方法,当前POST使用的是application/json方式,需要单独设置,通过日志可以看到已经请求成功。
执行了goods的countGood方法,参数:cc
MockHttpServletRequest:
HTTP Method = POST
Request URI = /countGood
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8",
Accept:"*/*", Content-Length:"31"]
Body = {"id":1,"name":"cc","status":2}
Session Attrs = {}Handler:
Type = com.example.junit5demo.controller.GoodsController
Method = com.example.junit5demo.controller.GoodsController#
countGood(Goods)
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:"text/plain;charset=UTF-8", Content
Length:"40"]
Content type = text/plain;charset=UTF-8
Body = countGood Goods(id=1, name=cc, status=2)
Forwarded URL = null
Redirected URL = null
Cookies = []
2021-07-10 15:54:18.096 INFO 15720 --- [ main]
c.e.j.controller.
GoodsControllerTest : status是:200,内容是:countGood Goods(id=1,
name=cc, status=2)
2021-07-10 15:54:18.119 INFO 15720 --- [extShutdownHook]
o.s.s.concurrent.
ThreadPoolTaskExecutor : Shutting down ExecutorService 'application
TaskExecutor'
通过以上的集成测试,完成了API从入口开始的全链路方法调用,验证了所有的方法调用,并完成了业务代码的测试,至此完成了Controller的集成测试过程。