Spring-boot官方给我们提供了测试用的场景启动器:spring-boot-starter-test
如果我们用IDEA创建一个spring项目,默认会给我们在pom里面引入这个依赖。其实有这个依赖就已经够了,因为它已经帮我们内置了Mock测试的核心mockito,也会帮我们引入junit,我这里的boot版本是2.5.7,junit用的5,所以后续都是在这个基础上做演示。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
项目部署前都需要先跑一遍test,一可以确认环境没问题,二可以保证业务代码能在我们预期内。所以写好测试代码很重要,很多开源项目的test都是特别优秀的模板,大家有空可以去学习学习。
mock就是模拟,模拟的目的是为了让我们专注于某一个方法的内部逻辑是否如我们预期,至于它内部又调用了其他方法应该返回什么,我们不关心,因为每一个方法都应该有对应的测试用例去覆盖,我们mock的就是跟它自身业务无关的一系列操作。
假如我们现在测试controller层的业务逻辑(其实controller层也没啥逻辑,常见的就是参数校验),controller内会注入service,如果我们只专注于controller的逻辑,就需要mock一个servcie实例去代替我们完成它内部的调用,从而让controller的业务逻辑排除service的干扰,即使service层洪水滔天,只要我们controller安然无恙就好,后面自会有service对应的测试方法去排查。
这样的好处是显而易见的:如果我们仅仅为了验证controller层的参数校验逻辑是不是对的,就要让service去执行它的逻辑,再调用数据访问层,假如sql写错了,我们就得花精力去改。而我们的初衷仅仅是想验证一下参数校验是不是写对了,走了太多弯路,非常难受。其次假如我们是一次update或者delete,测试完之后就得回滚,复杂场景下,transaction也帮不了我们,何况还有跟多不支持事务的数据库。除此还有很多延伸问题,这些问题有了mokito框架都可以帮我们解决。
直接上代码
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.Random.class)
class CollectsResourceTest {}
@SpringBootTest
: 必加,表示是一个springboot测试类,会准备好web环境,设置为随机端口防止跑测试代码的时候跟我们本地环境冲突。
@TestInstance
:必加,测试实例的生命周期,加上后@BeforeAll就可以加在非静态方法上了
@AutoConfigureMockMvc
:必加,mock测试的自动配置
@TestMethodOrder(MethodOrderer.Random.class)
:随意,设置测试代码的执行顺序
@Slf4j
:随意,lombok打日志的
@Autowired
private MockMvc mockMvc;
@MockBean
private CollectService collectService;
@BeforeAll
void login() throws Exception {
JsonObject json = new JsonObject();
json.addProperty("loginName", USER_NAME);
json.addProperty("password", PASSWORD);
mockMvc.perform(
post(URI.create("/login"))
.contentType(MediaType.APPLICATION_JSON)
.content(json.toString())
).andExpect(status().isOk()).andExpect(content().string(containsString("success"))).andDo(result -> {
String response = result.getResponse().getContentAsString(StandardCharsets.UTF_8);
TOKEN = JsonParser.parseString(response).getAsJsonObject().get("data").getAsJsonObject().get("token").toString().replaceAll("\"", "");
});
log.info("x-token:{}", TOKEN);
}
@Test
void delCollect() throws Exception {
when(collectService.undoCollect(anyString(), anyString())).thenReturn(true);
mockMvc.perform(
delete(URI.create("/collects/" + VIDEO_ID))
.header("x-token", TOKEN)
.accept(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8))
).andExpect(status().isOk()).andExpect(content().string(containsString("success")))
.andDo(result -> log.info("返回结果:{}", result.getResponse().getContentAsString(StandardCharsets.UTF_8)));
verify(collectService, times(1)).undoCollect(USER_NAME, VIDEO_ID);
log.info("{}已执行,验证通过","collectService.undoCollect()");
}
@MockBean
: 模拟一个service实例,测试的代码执行的时候不会真的去调用service的逻辑,而是需要我们给一个模拟的动作。
@BeforeAll
:所有测试用例执行前都要先跑这个,因为我们可能需要去登录接口先授权,才能访问其他接口。
when(collectService.undoCollect(anyString(), anyString())).thenReturn(true);
当collectService对象执行undoCollect方法的时候,让它返回我们想要的值。
verify(collectService, times(1)).undoCollect(USER_NAME, VIDEO_ID);
验证collectService的undoCollect方法是不是调用了一次。
verify(collectService, never()).undoCollect(USER_NAME, VIDEO_ID);
验证collectService的undoCollect方法是否未调用。
junit5
spring-boot-test web