简化你的Spring Boot开发:单元测试的秘诀
前言
本文是基于JUnit5
进行教学,希望各位读者可以通过文本学会如何测试自己的Spring Boot
项目,并在平时写代码如何注意代码的可测性。
Spring Boot UnitTest
Spring Boot
提供了许多实用的工具和注解来帮助我们完成单元测试,主要包括两大模块spring-boot-test
和spring-boot-test-autoconfigure
,我们可以通过依赖spring-boot-starter-test
来引入这两大模块,这其中包括了JUnit Jupiter
, AssertJ
, Hamcrest
,Mockito
以及一些其他实用的单元测试工具。
一个简单的例子
@SpringBootTest
class UserServiceTest {
@Autowired
UserService UserService;
@Test
void findUserById(){
User user = UserService.findUserById(3);
Assertions.assertNotNull(user);
}
}
用@SpringBootTest
注解标注测试类,通过@Autowired
注入UserService
,通过Assertions
对结果进行断言,这就是一个最简单的Spring Boot
单元测试。
@SpringBootTest
该注解会创建一个ApplicationContext
为测试提供一个上下文环境,所以在上面的例子中我们可以用@Autowired
来注入UserService
,该注解提供了几个属性来让用户进行一些自定义配置,如下:
String[] properties
和String[] value
properties
和value
互为别名,效果相同,为测试环境做一些配置,例如将web环境设置为reactive:
@SpringBootTest(properties = "spring.main.web-application-type=reactive")
class MyWebFluxTests {
// ...
}
String[] args
为测试程序引入一些参数,例如:
@SpringBootTest(args = "--app.test=one")
class MyApplicationArgumentTests {
@Test
void applicationArgumentsPopulated(@Autowired ApplicationArguments args) {
assertThat(args.getOptionNames()).containsOnly("app.test");
assertThat(args.getOptionValues("app.test")).containsOnly("one");
}
}
Class<?>[] classes
测试中需要注入的bean,常见的用法是创建一个Test的Spring Boot
启动类再注入到测试环境中
SpringBootTest.WebEnvironment webEnvironment
设置测试的web环境,是一个枚举类型,包括下面几个参数 :
MOCK
这是默认的选项,该选项会加载一个Web ApplicationContext
并且会提供一个mock的web环境,内置的容器不会启动。
RANDOM_PORT
加载一个WebServerApplicationContext
并且提供一个真实的web环境,内置容器会启动并监听一个随机的端口。
DEFINED_PORT
加载一个WebServerApplicationContext
并且提供一个真实的web环境,内置容器会启动并监听一个自定义的端口(默认8080)。
NONE
加载一个ApplicationContext
不提供任何web环境。
注意:在测试中使用@Transactional
注解可以在测试完成后回滚事务,但是RANDOM_PORT
和DEFINED_PORT
会提供真实的web环境,测试完成后不会回滚事务。
分层测试和代码可测性
分层测试
上面的例子只是一个简单的示例,很明显测试的属于3层架构中的service层,那什么是分层测试呢?
顾名思义,分层测试就是为程序的每一层都编写单元测试,虽然这样会花费更多的时间在编写单元测试上,但是能极大的保证代码的稳定性以及定位bug
位置,如果每个测试都是从controller
层开始的,那有些底层问题可能是很难发现的,所以建议大家在平时写单元测试时尽量进行分层测试。
代码可测性
代码可测性简单的讲就是编写单元测试的难易程度,如果你感觉你的代码写单元测试很困难,那就要思考你的代码是不是还可以进行优化,常见的测试不友好的代码有:
- 滥⽤可变全局变量
- 滥用静态方法
- 使用复杂的继承关系
- 代码高度耦合
仓储层测试
以Spring-Data-Jdbc
为例,只测试仓储层的代码,@DataJdbcTest
注解回配置一个内置的内存数据库以及注入JdbcTemplate
和Spring Data JDBC repositories
,不会引入web层等其他不必要的组建。
@DataJdbcTest
class UserMapperTest {
@Autowired
UserMapper userMapper;
@Test
public void test(){
userMapper.findById(1L)
.ifPresent(System.out::println);
}
}
web层测试
@WebMvcTest
注解会自动扫描@Controller
,@ControllerAdvice
,@JsonComponent
,Converter
,GenericConverter
,Filter
,HandlerInterceptor
,WebMvcConfigurer
和HandlerMethodArgumentResolver
并且会自动注入MockMvc
,我们可以利用MockMvc
对我们的web进行测试。
数据:
insert into USERS(`username`, `password`)
values ('1', '111'),
('2', '222'),
('3', '333'),
('4', '444'),
('5', '555');
controller层代码:
@RestController
@RequestMapping("/users")
@AllArgsConstructor
@Slf4j
public class UserController {
final UserService userService;
@GetMapping("/{id}")
public User findById(@PathVariable long id){
return userService.findUserById(id);
}
}
测试代码:
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
MockMvc mvc;
@MockBean
UserService userService;
@BeforeEach
public void mock(){
when(userService.findUserById(anyLong())).thenReturn(User.builder().username("test").password("test").build());
}
@Test
void exampleTest() throws Exception {
mvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andDo(print());
}
}
执行以后得到结果:
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"timestamp":0,"status":0,"message":null,"data":{"id":0,"username":"test","password":"test"}}
Forwarded URL = null
Redirected URL = null
Cookies = []
可以看出@WebMvcTest
为我们自动注入了MockMvc
,但是不会注入UserService
,所以我们需要mock UserService
,在mock()
方法中我们设置了对于任何Long
类型的入参都返回test
对象,因此我们并不会得到id = 1
的对象。
测试整个应用
分层测试完成后我们可能需要从上至下进行一个整体性测试以验证整个流程的可用性,利用@SpringBootTest
注解注入整个测试环境的ApplicationContext
,通过@AutoConfigureMockMvc
引入MockMvc
。
@AutoConfigureMockMvc
和@WebMvcTest
的区别在于@AutoConfigureMockMvc
只是单纯的注入MockMvc
而@WebMvcTest
会同时引入web层的ApplicationContext
(注意仅仅是web层的上下文环境,所以我们才需要mock其他组件)。
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Test
void exampleTest(@Autowired MockMvc mvc) throws Exception {
mvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andDo(MockMvcResultHandlers.print());
}
}
运行后的结果(我对返回进行进行了包装,自动加入了timestamp
、status
、message
等字段,可以忽略)
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"timestamp":0,"status":0,"message":null,"data":{"id":1,"username":"1","password":"111"}}
Forwarded URL = null
Redirected URL = null
Cookies = []
可以看到在Body中我们得到了id = 1
的对象,说明我们取得了真实的数据。
推荐阅读
-
Spring Boot:中小型医院网站开发的新趋势
-
Spring Boot:中小型医院网站的敏捷开发
-
基于 Spring Boot 的医疗病历 B2B 平台开发战略
-
Spring Boot 在甘肃非遗文化网站开发中的应用
-
Java语言编写的B2B2C商城源码,支持多商家入驻、直播带货、新零售模式、O2O商城、电子商务、拼团、分销、直播和短视频功能,基于Spring Boot开发。
-
Spring Boot项目搭建指南:轻松实现JSP与注解开发的完美兼容,包含详细步骤与两种配置方法
-
用Spring Boot和VUE开发智能客服系统的源代码
-
实战 Spring Boot:单元测试中的断言技巧
-
使用Spring Boot开发的云HIS系统:基层医院卫生服务机构信息管理系统的完整源代码
-
简化你的Java编程:使用JUnit进行单元测试