一、在SpringBoot项目中引入单元测试框架
在做系统的自动化持续集成的时候,会要求自动的做单元测试,只有所有的单元测试都跑通了,才能打包构建。单元测试是软件测试的基础,因此单元测试的效果会直接影响到软件的后期测试,最终在很大程度上影响到产品的质量。在SpringBoot项目中,可以通过新增maven依赖将单元测试框架依赖进来:
<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>
在SpringBoot2中,该依赖自动将JUnit5
和Mockito
测试框架依赖进来。
二、JUnit测试框架
JUnit是Java领域经典的测试框架,目前最新版本是JUnit5,采用了Java8的编程风格并且比JUnit4更加健壮和灵活。在开始书写测试代码之前,我们先回顾一下JUnit常用的测试注解。在JUnit4和JUnit5中,注解的写法有些许变化。
特性 | JUnit4 | JUnit5 |
---|---|---|
声明一个测试方法 | @Test | @Test |
在当前类的所有测试方法执行前要执行的方法 | @BeforeClass | @BeforeAll |
在当前类的所有测试方法执行后要执行的方法 | @AfterClass | @AfterAll |
每个测试方法执行前要执行的方法 | @Before | @BeforeEach |
每个测试方法执行后要执行的方法 | @After | @AfterEach |
忽略某个测试方法或测试类 | @Ignore | @Disabled |
动态测试用例生成工厂 | 无此特性 | @TestFactory |
嵌套测试 | 无此特性 | @Nested |
标记与过滤 | @Category | @Tag |
注册定制扩展点 | 无此特性 | @ExtendWith |
三、Mockito测试框架
Mockito框架可以创建和配置mock对象来模拟对象的行为,与JUnit结合使用,使用Mockito简化了具有外部依赖的类的测试开发。Mockito测试框架可以帮助我们模拟HTTP请求,从而达到在服务端测试目的。因为其不会真的去发送HTTP请求,而是模拟HTTP请求内容,从而节省了HTTP请求的网络传输,测试速度更快。
3.1 Mockito基本用法
@Slf4j
public class ArticleControllerTest {
//mock对象
private static MockMvc mockMvc;
//在所有测试方法执行之前进行mock对象初始化
@BeforeAll
static void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(new ArticleController()).build();
}
//测试方法
@Test
public void saveArticle() throws Exception {
String articleParam = "{\n" +
" \"id\": 1,\n" +
" \"author\": \"William\",\n" +
" \"title\": \"spring boot\",\n" +
" \"content\": \"c\",\n" +
" \"createTime\": \"2023-06-06 05:23:34\",\n" +
" \"reader\":[{\"name\":\"Jerry\",\"age\":18},{\"name\":\"Jack\",\"age\":37}]\n" +
"}";
MvcResult result = mockMvc.perform(
MockMvcRequestBuilders
.request(HttpMethod.POST, "/rest/articles")
.contentType("application/json")
.content(articleParam)
)
.andExpect(MockMvcResultMatchers.status().isOk()) //HTTP:status 200
.andExpect(MockMvcResultMatchers.jsonPath("$.data.author").value("William"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.reader[0].age").value(18))
.andDo(print())
.andReturn();
result.getResponse().setCharacterEncoding("UTF-8");
log.info(result.getResponse().getContentAsString());
}
}
MockMvc对象有以下几个基本的方法:
- perform : 模拟执行一个RequestBuilder构建的HTTP请求,会执行SpringMVC的流程并映射到相应的控制器Controller执行。
- contentType:发送请求内容的序列化的格式,"application/json"表示JSON数据格式
- andExpect: 添加RequsetMatcher验证规则,验证控制器执行完成后结果是否正确,或者说是结果是否与我们期望(Expect)的一致。
- andDo: 添加ResultHandler结果处理器,比如调试时打印结果到控制台
- andReturn: 最后返回相应的MvcResult,然后进行自定义验证/进行下一步的异步处理
上面的整个过程,我们都没有使用到Spring Context依赖注入、也没有启动tomcat web容器。整个测试的过程十分的轻量级,速度很快。
3.2 在真实servlet容器环境下Mock测试
上面的测试执行速度非常快,但是有一个问题:它没有启动servlet容器和Spring 上下文,自然也就无法实现依赖注入(不支持@Resource和@AutoWired注解),这就导致它在全流程测试中有很大的局限性。同时如果ArticleController依赖于ArticleService对象,但是ArticleService代码还没写是一个空类空方法不能用,我们就可以mock一个ArticleService来完成测试。
ArticleService接口如下:
public interface ArticleService {
public String saveArticle(Article article);
}
ArticleController 如下:
@Slf4j
@RequestMapping("/rest")
@RestController
public class ArticleController {
@Resource
private ArticleService articleService;
@PostMapping("/articles")
public AjaxResponse saveArticle(@RequestBody Article article){
log.info("saveArticle:" + article);
Article result = articleService.saveArticle(article);
return AjaxResponse.success(result);
}
}
测试类如下:
@Slf4j
@AutoConfigureMockMvc
@SpringBootTest
@ExtendWith(SpringExtension.class)
public class ArticleControllerTest {
@Resource
private MockMvc mockMvc;
@MockBean
private ArticleService articleService;
//测试方法
@Test
public void saveArticle() throws Exception {
String articleParam = "{\n" +
" \"id\": 1,\n" +
" \"author\": \"William\",\n" +
" \"title\": \"spring boot\",\n" +
" \"content\": \"c\",\n" +
" \"createTime\": \"2023-06-06 05:23:34\",\n" +
" \"reader\":[{\"name\":\"Jerry\",\"age\":18},{\"name\":\"Jack\",\"age\":37}]\n" +
"}";
ObjectMapper mapper = new ObjectMapper();
Article article = mapper.readValue(articleParam , Article.class);
Mockito.when(articleService.saveArticle(article)).thenReturn(article);
MvcResult result = mockMvc.perform(
MockMvcRequestBuilders
.request(HttpMethod.POST, "/rest/articles")
.contentType("application/json")
.content(article)
)
.andExpect(MockMvcResultMatchers.status().isOk()) //HTTP:status 200
.andExpect(MockMvcResultMatchers.jsonPath("$.data.author").value("William"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.reader[0].age").value(18))
.andDo(MockMvcResultHandlers.print())
.andReturn();
result.getResponse().setCharacterEncoding("UTF-8");
log.info(result.getResponse().getContentAsString());
}
}
- @AutoConfigureMockMvc,该注解表示对MockMvc进行配置,MockMvc对象由spring 依赖注入构建;
- @SpringBootTest,是用来创建Spring的上下文ApplicationContext,保证测试在上下文环境里运行;@SpringBootTest 注解包含了 @ExtendWith注解,为我们构造了一个的Servlet容器运行运行环境,并在此环境下测试。
- @MockBean 可以用MockBean伪造模拟一个Service ,上例中Mock了一个articleService对象。
-
Mockito.when(articleService.saveArticle(articleObj)).thenReturn(articleObj);
这是一行打桩代码,告诉测试用例程序,当你调用articleService.saveArticle(article)方法的时候,不要去真的调用这个方法,直接返回一个结果(article)就好了。
该测试方法会真实的启动一个tomcat容器、以及Spring 上下文,所以我们可以进行依赖注入(@Resource),但是有一个非常明显的缺点:每次做一个接口测试,都会真实的启动一次servlet容器,Spring上下文加载项目里面定义的所有的Bean,导致执行过程很缓慢。
四、轻量级测试
在ExtendWith的AutoConfigureMockMvc注解的共同作用下,启动了SpringMVC的运行容器,并且把项目中所有的@Bean全部都注入进来。单把所有的bean都注入进来会很臃肿且会拖慢单元测试的效率。如果我只是想测试一下控制层Controller,怎么办?有没有轻量级的解决方案?答案是有的,我们只需要使用@WebMvcTest替换@SpringBootTest注解即可:
- @SpringBootTest注解告诉SpringBoot去寻找一个主配置类(例如带有@SpringBootApplication的配置类),并使用它来启动Spring应用程序上下文。SpringBootTest加载完整的应用程序并注入所有可能的bean,因此速度会很慢。
- @WebMvcTest注解主要用于controller层测试,只覆盖应用程序的controller层,@WebMvcTest(ArticleController.class)只加载ArticleController这一个Bean用作测试。所以WebMvcTest要快得多,因为我们只加载了应用程序的一小部分。
网友评论