本章学习下Spring Boot 单元测试的简单使用。
1 简单demo
环境:
jdk1.8
idea2020.1
Spring Boot 2.3.3
junit5
项目结构如下:
pom.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.jt.learn</groupId>
<artifactId>springboot-learn-demo2-junittest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-learn-demo2-junittest</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
model
public class User {
private String name;
private Integer age;
private String address;
public User() {
}
public User(String name, Integer age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
}
SayService
public interface SayService {
public String sayHello(User user);
}
SayServiceImpl
@Service
public class SayServiceImpl implements SayService {
@Override
public String sayHello(User user) {
return "hello!"+ user.getName()+"&"+user.getAge()+"&"+user.getAddress();
}
}
DemoService
public interface DemoService {
public User getUser();
}
DemoServiceImpl
@Service
public class DemoServiceImpl implements DemoService {
@Override
public User getUser() {
User user = new User("jack", 25, "火星");
return user;
}
}
controller
@RestController
public class SayController {
@Autowired
private SayService sayService;
@RequestMapping("/hello")
public String hello(User user) {
return sayService.sayHello(user);
}
}
2 service 测试
2.1 使用@SpringBootTest测试
@SpringBootTest
public class SayServiceTest {
@Autowired
private SayService sayService;
@Test
void test() {
String result = sayService.sayHello(new User("Tom", 18, "大风车"));
Assertions.assertEquals(result, "hello!Tom&18&大风车");
}
}
@SpringBootTest:启动spring容器,自动扫描当前包及上级包中的@SpringBootApplication注解。
对于返回结果我们可以使用junit的断言来判断,如上面的Assertions.assertEquals,
判断两个类型对象否相等;当然还有其他的一些断言,如:
assertNotEquals:判断两个类型的对象是否不相等。
assertTrue:判断boolean类型是否为true。
assertThrows:判断抛出异常类型是否为期望类型。
assertNull:判断是否为空。
等等,还有许多。在单元测试中可以使用这些断言快速判断调用接口的返回是否和我们期望的一样。
现在我们再来做一个测试,在我们的DemoServiceImpl和SayServiceImpl中添加无参构造方法,并打印信息
public DemoServiceImpl() {
System.out.println("===================== DemoServiceImpl 实例化 ===========================");
}
public SayServiceImpl() {
System.out.println("===================== SayServiceImpl 实例化 ===========================");
}
运行测试方法,控制台输出如下:
可以看到虽然我们测试方法虽然没有依赖DemoServiceImpl,但是还是会被实例化。
使用SpringBootTest注解,当项目启动完成后才会调用测试方法,如果项目小影响不是很大,但是如果我们项目比较大,装配的bean比较多,还要执行一些初始化方法,这样我们测试一个方法就会非常浪费时间。
这种情况我们可以在测试类上加入@ContextConfiguration注解,可以仅将我们测试需要的bean装配进容器,减少了容器初始化时间,大大提高了效率,比如现在我需要测试SayServiceImpl的功能,配置如下
@SpringBootTest
@ContextConfiguration(classes = {
SayServiceImpl.class})
public class SayServiceTest {
@Autowired
private SayService sayService;
@Test
void test() {
String result = sayService.sayHello(new User("Tom", 18, "大风车"));
Assertions.assertEquals(result, "hello!Tom&18&大风车");
}
}
运行测试方法,查看控制台,可以看到现在只实例化了SayServiceImpl,没有实例化DemoServiceImpl
2.2 使用@SpringBootTest+mock测试
在我们进行单元测试的时候,我们只想测试这个测试类的逻辑,而不关注依赖对象的逻辑,这时候就需要用mock模拟出依赖对象。
代码如下:
UserDao
public interface UserDao {
public User getUser(String name);
}
UserDaoImpl
@Repository
public class UserDaoImpl implements UserDao {
// 省略从数据库查询数据,直接返回数据
@Override
public User getUser(String name) {
return new User(name, 19, "来自dao方法返回的数据");
}
}
UserService
public interface UserService {
public User getUser(String name);
}
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public User getUser(String name) {
return userDao.getUser(name);
}
}
对UserService的方法进行测试,但是其依赖了UserDao,我们不关心UserDao的逻辑,现在我们对UserDao进行mock,mock过后将不会再执行UserDao的方法逻辑。
不使用mock直接测试
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
void test() {
System.out.println(userService.getUser("jerry"));
}
}
控制台输出如下:
可以看出返回的User对象为我们UserDao中new的User对象。
使用mock:
本例中使用了junit5的常用注解如:@BeforeEach,将会在下文中说明。
@SpringBootTest
public class UserServiceTest1 {
@Autowired
UserService userService;
// 替换 spring 中 userDao
@MockBean
UserDao userDao;
// 在执行每个测试方法之前执行
@BeforeEach
void beforeEachTest() {
// 表示调用userDao.getUser方法传入任何值都返回new User("Tom", 22, "来自Mock返回的数据")
Mockito.when(userDao.getUser(Mockito.any(String.class)))
.thenReturn(new User("Tom", 22, "来自Mock返回的数据"));
}
@Test
void test() {
System.out.println(userService.getUser("mock"));
}
}
执行测试方法控制台打印如下:
可一看出返回的数据是我们使用Mock返回的结果,并没有执行UserDao.getUser方法的逻辑。
2.3 只使用mock
以上两种方式都会启动spring容器,测试起来非常耗时,我们可以直接使用Mock不依赖spring容器进行测试,代码如下:
public class UserServiceTest2 {
// 创建一个实例 ,且使用@Mock创建的mock会被注入进来
@InjectMocks
private UserService userService = new UserServiceImpl();
// 创建一个mock,会被注入到@InjectMocks修饰的userService
@Mock
private UserDao userDao;
// 每个测试方法运行前执行
@BeforeEach
public void BeforeEachTest() {
// 必须进行初始化或者使用@RunWith(MockitoJUnitRunner.class)则可省略这行代码
MockitoAnnotations.initMocks(this);
// 表示调用userDao.getUser方法传入任何值都返回new User("Tom", 22, "来自Mock返回的数据")
Mockito.when(userDao.getUser(Mockito.any(String.class)))
.thenReturn(new User("Tom", 22, "来自Mock返回的数据"));
}
@Test
public void test() {
System.out.println(userService.getUser("mock"));
}
}
现在类上没有@SpringBootTest注解,示例中:
@InjectMocks 修饰的变量是我们自己new出来的,所以会调用本来的逻辑,但是使用了该注解会将使用@Mock创建的mock注入进来,即将userDao注入到userService 。
运行测试方法,控制台如下:
可以看出没有加载spring容器,直接运行的测试方法。返回的mock数据。
3 controller 测试
本节介绍通过模拟http请求,使用网络形式,转发到controller进行调用。
3.1 使用Mock
案例使用本文开始介绍的SayController 进行测试。
@RestController
public class SayController {
@Autowired
private SayService sayService;
@RequestMapping("/hello")
public String hello(@RequestBody User user) {
return sayService.sayHello(user);
}
}
测试类如下:
@SpringBootTest
public class SayControllerTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
@BeforeEach
public void beforeEachTest() {
// 必须初始化mock
MockitoAnnotations.initMocks(this);
// 创建MockMvc实例
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
public void test() throws Exception {
MvcResult mvcResult = mockMvc.perform(
// 模拟一个请求
MockMvcRequestBuilders.post("/hello")
//定义客户端希望接受到的数据
.accept(MediaType.APPLICATION_JSON_UTF8)
// 定义客户端发送的数据格式
.contentType(MediaType.APPLICATION_JSON_UTF8)
// 请求的参数
.content("{\"name\":\"Tom\",\"age\":\"22\",\"address\":\"来自Mock模拟请求的数据\"}")
)
//期望响应状态码为200
.andExpect(MockMvcResultMatchers.status().isOk())
//期望返回的数据
.andExpect(MockMvcResultMatchers.content().string("hello!Tom&22&来自Mock模拟请求的数据"))
// 接收返回结果
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}
}
定义@BeforeEach方法初始化Mock,创建MockMvc实例,必须初始化,否则报错。也可以直接使用@AutoConfigureMockMvc自动配置注解,配置了此注解就不用显示的初始化和创建实例,代码如下:
@SpringBootTest
@AutoConfigureMockMvc
public class SayControllerTest1 {
@Autowired
private MockMvc mockMvc;
@Test
public void test() throws Exception {
MvcResult mvcResult = mockMvc.perform(
// 模拟一个请求
MockMvcRequestBuilders.post("/hello")
//定义客户端希望接受到的数据
.accept(MediaType.APPLICATION_JSON)
// 定义客户端发送的数据格式
.contentType(MediaType.APPLICATION_JSON_UTF8)
// 请求的参数
.content("{\"name\":\"Tom\",\"age\":\"22\",\"address\":\"来自Mock模拟请求的数据\"}")
)
//期望响应状态码为200
.andExpect(MockMvcResultMatchers.status().isOk())
//期望返回的数据
.andExpect(MockMvcResultMatchers.content().string("hello!Tom&22&来自Mock模拟请求的数据"))
// 接收返回结果
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}
}
代码中使用的API已加上了注释。
3.2 使用WebTestClient
首先在pom.xml中加入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
测试代码如下:
//配置本地随机端口
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//WebTestClient 自动配置
@AutoConfigureWebTestClient
public class SayControllerTest2 {
@Autowired
private WebTestClient webTestClient;
@Test
public void test() {
//模拟 一个post请求
EntityExchangeResult<String> result = webTestClient.post().uri("/hello")
//设置客户端请求发送的数据类型
.contentType(MediaType.APPLICATION_JSON_UTF8)
//设置客户端请求接收的数据类型
.accept(MediaType.APPLICATION_JSON_UTF8)
//设置客户端请求的数据
.bodyValue("{\"name\":\"Tom\",\"age\":\"22\",\"address\":\"来自Mock模拟请求的数据\"}")
//发送请求
.exchange()
// 期望返回的状态码
.expectStatus().isOk()
// 期望返回的数据类型和内容
.expectBody(String.class).isEqualTo("hello!Tom&22&来自Mock模拟请求的数据")
// 返回结果
.returnResult();
System.out.println("返回结果:" + result.getResponseBody());
}
}
注意需要在@SpringBootTest中指定环境参数,并加上WebTestClient自动配置注解@AutoConfigureWebTestClient,
使用到WebTestClient的API在代码中有详细注释。
4 junit5常用的注解
4.1 @BeforeEach
@BeforeEach执行每个测试方法之前都会执行。
测试代码:
@SpringBootTest
public class AnnotationTest {
@BeforeEach
public void beforeEachTest() {
System.out.println("+++++++++++执行 BeforeEach +++++++++++++");
}
@Test
public void test1() {
System.out.println("======== 执行test1 =========");
}
@Test
public void test2() {
System.out.println("======== 执行test2 =========");
}
}
直接点击类上的运行图标,执行所有测试方法
执行效果如下:
4.2 @BeforeAll
@BeforeAll 执行所有测试方法时,在所有测试方法执行前仅执行一次,且只能修饰静态方法,或者在测试类上加入@TestInstance(TestInstance.Lifecycle.PER_CLASS)
注解,可修饰非静态方法。
测试代码如下:
修饰静态方法
@SpringBootTest
public class AnnotationTest {
@BeforeAll
public static void beforeAllTest() {
System.out.println("+++++++++++执行 BeforeAll +++++++++++++");
}
@BeforeEach
public void beforeEachTest() {
System.out.println("+++++++++++执行 BeforeEach +++++++++++++");
}
@Test
public void test1() {
System.out.println("======== 执行test1 =========");
}
@Test
public void test2() {
System.out.println("======== 执行test2 =========");
}
}
修饰非静态方法
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
public class AnnotationTest {
@BeforeAll
public void beforeAllTest() {
System.out.println("+++++++++++执行 BeforeAll +++++++++++++");
}
@BeforeEach
public void beforeEachTest() {
System.out.println("+++++++++++执行 BeforeEach +++++++++++++");
}
@Test
public void test1() {
System.out.println("======== 执行test1 =========");
}
@Test
public void test2() {
System.out.println("======== 执行test2 =========");
}
}
效果如下:
@AfterAll与 @BeforeAll类似,在所有测试方法之后执行
@AfterEach 与@BeforeEach类似,在每个测试方法之后执行
这里就不一一演示。
4.3 @TestMethodOrde
在执行所有测试方法时可以使用@TestMethodOrder改变方法的执行顺序。
如:
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AnnotationTest1 {
@Test
@Order(2)
public void test1() {
System.out.println("======== 执行test1 =========");
}
@Test
@Order(1)
public void test2() {
System.out.println("======== 执行test2 =========");
}
}
效果如下:
先执行了test2,后执行test1,这里我们在@TestMethodOrder中传入了MethodOrderer.OrderAnnotation.class参数,需要和@Order配合,@Order传入的值越小越先执行。
@TestMethodOrder还可传入其它参数来排序:
MethodOrderer.Random.class :随机执行。
MethodOrderer.Alphanumeric.class:根据字母数字排序。
4.3 @Disabled
跳过所修饰的测试方法