测试是企业软件开发不可缺少的一部分。
翻开任何一个优秀的开源框架源码,会发现在测试的包里面有不亚于源码的代码量。如何快速的编写出针对性的测试代码,也是一门绝活。
这里不展开讲解Mockito等测试框架,只针对Spring Boot应用,给出Spring Boot开发中常用的测试方法,帮助你进行快速测试开发。
导入依赖
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Gradle
testImplemention "org.springframework.boot:spring-boot-starter-test"
注解
@SpringBootTest : 从当前的标记该注解的测试类开始找,直至找到@SpringBootApplication或者@SpringBootConfiguration。就从标记了上述两个注解的类开始扫描bean。
也就是说,你可以在Test类里面自定义项目启动类。
比如:
下面是你的项目启动入口
@SpringBootApplication
@Import(ClassA.class)
public class DemoSpringApplication {
public static void main(String[] args) {
new SpringApplication(DemoSpringApplication.class).run(args);
}
}
如果你的测试类如下
@SpringBootTest
public class WhereToScanTest {
@Test
void works(@Autowired ApplicationContext applicationContext){
Assertions.assertThat(applicationContext.getBean(ClassA.class)).isNotNull();
}
}
那么DemoSpringApplication就会是应用启动类,能够测试通过。
但是如果,你在WhereToScanTest该包下创建一个@SpringConfiguration注解的类,只是简单加上一个@SpringBootConfiguration注解,测试就会失败。
@SpringBootTest
public class WhereToScanTest {
@Test
void works(@Autowired ApplicationContext applicationContext){
Assertions.assertThat(applicationContext.getBean(ClassA.class)).isNotNull();
}
@SpringBootConfiguration
static class MyConfig{
}
}
这是因为只扫描标记了@SpringBootConfiguration的MyConfig类下的包,并不会创建ClassA的bean。
使用参数
@SpringBootTest(args = "--app.test=true")
public class ArgumentTest {
@Test
void argsWorks(@Autowired ApplicationArguments args){
assertThat(args.getOptionNames().contains("app.test"));
assertThat(args.getOptionValues("app.test")).containsOnly("true");
}
}
测试web应用程序
如果只是用@SpringBootTest注解,不会开启web环境,如果想要测试web代码,可以加上@AutoConfigureMockMvc注解。
实例代码:
@RestController
public class DemoController {
@RequestMapping("/hello")
public String hello(){
return "Hello from demo";
}
}
@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcTest {
@Test
void mockMvcWorks(@Autowired MockMvc mockMvc) throws Exception {
mockMvc.perform(get("/hello")).andExpect(status().isOk()).andExpect(content().string("Hello from demo"));
}
}
注意:这会扫描所有的spring注解并实例化完整的ApplicationContext,也就是启动整个Spring应用,如果你想只测试mvc部分,可以考虑使用@WebMvcTest。该测试是启动一个mock的web环境。
如果想测试真实的sever,使用如下注解,推荐使用一个随机端口进行测试。
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RunningServerTest {
private WebTestClient webTestClient;
@LocalServerPort
private int port;
private String url;
@BeforeEach
void before(){
this.url = "http://localhost:" + port;
this.webTestClient = WebTestClient.bindToServer().responseTimeout(Duration.ofSeconds(10)).baseUrl(url).build();
}
@Test
void runningServerWorks(@Autowired WebTestClient webTestClient){
this.webTestClient.get().uri("/hello").exchange().expectStatus().isOk().expectBody().equals("Hello from demo");
}
}
其中@LocalServerPort会将程序启动的端口注入到port字段里去。
Mock
适合哪种情况?某些服务在开发环境无法调用,那么就需要mock,mock意思是模拟,也就是说模拟某些bean来进行你想要的测试。
例如你定义了一个远程访问的service,但是开发环境无法调通,则可以模拟。
@MockBean的用法
@SpringBootTest
public class MockBeanTests {
@MockBean
private RemoterService remoterService;
@Test
void mockBeanWorks(){
//如果调用sayHello方法,返回Hello mock bean
given(remoterService.sayHello()).willReturn("Hello mock bean");
String s = this.remoterService.sayHello();
assertThat(s).isEqualTo("Hello mock bean");
}
}
@MockBean 向测试程序注入了一个RemoteService的Bean,但是具体怎么定义方法怎么执行是需要你来说明的。其中given()方法是Mockito测试框架的方法,意思是如果调用remoteService的sayHello方法,就返回“Hello mock bean”。
分模块测试(WebMVC)
如果使用SpringBootTest,就是扫描整个应用内的bean。在一个项目中可能有很多的Spring Boot Starter,例如只想测试mvc,而不想测试jdbc,那么就需要使用@...Test。
使用@WebMvcTest注解,只会自动配置webmvc相关的功能,只会扫描如下的Bean。
@Controller
@ControllerAdvice
@JsonComponent
Converter
Filter
WebMvcConfigure
...
指定只测试某个Controller:
@WebMvcTest(UserController.class)
public class MockMvcTest {
@MockBean
private UserService userService;
@Autowired
private MockMvc mvc;
@Test
void mockMvcWorks(@Autowired MockMvc mockMvc) throws Exception {
mockMvc.perform(get("/hello")).andExpect(status().isOk()).andExpect(content().string("Hello from demo"));
}
@Test
void mcvWorks() throws Exception{
given(userService.getUserName()).willReturn("Tyler");
this.mvc.perform(get("/user/name")).andExpect(status().isOk()).andExpect(content().string("Tyler"));
}
}
@WebMvcTest(xxxController.class) 只向web中添加该controller,例如该例子只会有UserController,如果还有其他Controller定义其他的@RequestMapping,在测试程序中访问是会404,因为这里我们只定义加载了UserController。
分模块测试(Data JPA )
和上面的mvc模块一样,@DataJpaTest也是只开启JPA相关自动配置,只扫描@Entinty和JpaRepository。使用@DataJpaTest在会回退事务,所以不用担心会向数据库插入无效的数据,默认该注解会使用内嵌的内存数据库,如果想要使用你本地的例如localshot:3306数据库,需要使用如下注解。
可以注入TestEntityManager进行一些操作,也可以注入测试自定义的Repository。
@AutoConfigureTestDatabase(replace = Replace.NONE)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
class MyRepositoryTests {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository repository;
@Test
void testExample() throws Exception {
this.entityManager.persist(new User("sboot", "1234"));
User user = this.repository.findByUsername("sboot");
assertThat(user.getUsername()).isEqualTo("sboot");
assertThat(user.getEmployeeNumber()).isEqualTo("1234");
}
}
Spring其他测试方法
如果你什么注解也不想用,既不想测试Data JPA 也不想测试 mvc,只是想注册几个bean,然后启动做些测试,那么也可以用下面两个类。
可以用ApplicationContextRunner,该类是一个标准的,无web的环境。
可以直接用ApplicationContext,该类是Spring为应用程序提供配置的核心接口,例如AnnotationConfigApplicationContext。
用法如下:
public class ApplicationContextRunnerTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
@Test
void works(){
contextRunner.withBean("classa",ClassA.class).run((context -> {
ClassA bean = context.getBean(ClassA.class);
assertThat(context).hasBean("classA");
}));
}
}
public class AnnotationConfigTests {
private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@Test
void works(){
this.context.register(ClassA.class);
this.context.refresh();
Assertions.assertThat(this.context.containsBean("classA")).isTrue();
}
}
总结
能够写出有针对性的测试代码,其实也不是一件容易的事,如果你对代码质量有较高要求,代码层面测试是不可缺少的一部分。希望这篇文章能帮到你一二。这里只是大概列出了一些测试案例,养成代码测试的习惯,更多测试的技巧可以在不断的测试中自己挖掘。