前言
本文是基于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的对象,说明我们取得了真实的数据。