一、版本差异
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库 ,在 Spring Boot 2.2.0 版本之前,spring-boot-starter-test 包含了 JUnit 4 的依赖,Spring Boot 2.2.0 版本之后替换成了 Junit Jupiter。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
导入的依赖如下: 可以看到,SpringBootTest默认集成了以下功能:
- JUnit 5: Java单元测试框架
- Spring Test & Spring Boot Test: Spring Boot的测试工具和支持
- AssertJ: 流式断言
- Hamcrest: Hamcrest断言
- Mockito: Java Mock框架
- JSONassert: JSON断言
- JsonPath: XPath for JSON
二、SpringBootTest和Junit5的使用
整体上,Spring Boot Test支持的测试种类,大致可以分为如下三类 :
- 单元测试:
一般面向方法,编写一般业务代码时,测试成本较大 。涉及到的注解有@Test。 - 切片测试:一般面向难于测试的边界功能,介于单元测试和功能测试之间。涉及到的注解有 @WebMvcTest等。
主要就是对于Controller的测试,分离了Service层,这里就涉及到Moc控制层所依赖的组件了 - 功能测试:一般
面向某个完整的业务功能,同时也可以使用切面测试中的mock能力,推荐使用 。涉及到的注解有@SpringBootTest等。
- 单元测试
集成测试,不启动server ,以创建项目后自动生成的默认测试类为例:
@SpringBootTest
class TestDemoApplicationTests {
@Test
void contextLoads() {
}
}
默认无参数的@SpringBootTest 注解会加载一个Web Application Context并提供Mock Web Environment,但是不会启动内置的server 。这点从日志中没有打印Tomcat started on port(s)可以佐证。
- 集成测试,启动server
新建一个测试类如下:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DemoTest {
@LocalServerPort
private Integer port;
@Test
@DisplayName("should access application")
void shouldAccessApplication() {
assertThat(port).isGreaterThan(1024);
}
}
也可以通过指定@SpringBootTest的Web Environment为DEFINED_PORT 来指定server侦听应用程序配置的端口,默认为8080。不过这种指定端口的方式很少使用,因为如果本地同时启动应用时,会导致端口冲突 。
- 更多关系JUnit5集成SpringBootTest的例子,参考这个文档,我这里不在啰嗦
三、Spring Boot Test中的主要注解
- 在说Mockito之前,先看一下SpringBootTest的注解,
Mockito是一个独立的框架,被springboot集成了而已。
从功能上讲,Spring Boot Test中的注解主要分如下几类
使用@SpringBootApplication启动测试或者生产代码,被@TestComponent描述的Bean会自动被排除掉。 如果不是则需要向@SpringBootApplication添加TypeExcludeFilter。
- mock类型的注解
@MockBean和@SpyBean这两个注解,在mockito框架中本来已经存在,且功能基本相同。Spring Boot Test又定义一份重复的注解,目的在于使MockBean和SpyBean被ApplicationContext管理 ,从而方便使用。
MockBean和SpyBean功能非常相似,都能模拟方法的各种行为。不同之处在于MockBean是全新的对象,跟正式对象没有关系;而SpyBean与正式对象紧密联系,可以模拟正式对象的部分方法 ,没有被模拟的方法仍然可以运行正式代码。
- 自动配置类型的注解(@AutoConfigure*)
这些注解可以搭配@\*Test使用 ,用于开启在@\*Test中未自动配置的功能 。例如@SpringBootTest和@AutoConfigureMockMvc组合后,就可以注入org.springframework.test.web.servlet.MockMvc。
“自动配置类型”有两种使用方式:
- 在功能测试(即使用@SpringBootTest)时显示添加。
一般在切片测试中被隐式使用,例如@WebMvcTest注解时,隐式添加了@AutoConfigureCache、@AutoConfigureWebMvc、@AutoConfigureMockMvc。
所有的@*Test注解都被@BootstrapWith注解,它们可以启动ApplicationContext,是测试的入口 ,所有的测试类必须声明一个@*Test注解。
除了@SpringBootTest之外的注解都是用来进行切面测试的,他们会默认导入一些自动配置,点击官方docs查看详情。一般情况下,推荐使用@SpringBootTest而非其它切片测试的注解,简单有效。 若某次改动仅涉及特定切片,可以考虑使用切片测试。SpringBootTest是这些注解中最常用的一个,其中包含的配置项如下:
webEnvironment详细说明:
@TestComment vs @Comment : @TestComponent是另一种@Component,在语义上用来指定某个Bean是专门用于测试的。使用@SpringBootApplication服务时,@TestComponent会被自动排除@TestConfiguration vs @Configuration : @TestConfiguration是Spring Boot Boot Test提供的,@Configuration是Spring Framework提供的。@TestConfiguration实际上是也是一种@TestComponent,只是这个@TestComponent专门用来做配置用。 @TestConfiguration和@Configuration不同,它不会阻止@SpringBootTest的查找机制,相当于是对既有配置的补充或覆盖。@SpringBootTest vs @WebMvcTest(或@*Test) : 都可以启动Spring的ApplicationContext @SpringBootTest自动侦测并加载@SpringBootApplication或@SpringBootConfiguration中的配置,@WebMvcTest不侦测配置,只是默认加载一些自动配置。 @SpringBootTest测试范围一般比@WebMvcTest大。@MockBean vs @SpyBean : 都能模拟方法的各种行为。不同之处在于MockBean是全新的对象,跟正式对象没有关系;而SpyBean与正式对象紧密联系,可以模拟正式对象的部分方法,没有被模拟的方法仍然可以运行正式代码
参考文章
四、Mockito的使用
- 简单的一个例子
public class MyMockitoTest {
private static UserServiceImpl mockUserService;
private static List<String> mockedList;
@BeforeAll
public static void beforeMock() throws Exception {
mockUserService = mock(UserServiceImpl.class);
mockedList = mock(List.class);
when(mockUserService.getOneUser(1)).thenReturn(new User("a",1));
when(mockUserService.getOneUser(2)).thenThrow(new IllegalAccessException());
when(mockUserService.getOneUser(3)).thenReturn(new User("a",1));
when(mockUserService.update(isA(User.class))).thenReturn(true);
}
@Test
@DisplayName("GetOneUser")
public void testGet() throws Exception {
User user = mockUserService.getOneUser(1);
User oneUser = mockUserService.getOneUser(2);
User oneUser1 = mockUserService.getOneUser(3);
System.out.println(user);
System.out.println(oneUser);
mockUserService.update(user);
verify(mockUserService, times(1)).getOneUser(eq(1));
verify(mockUserService, times(1)).update(isA(User.class));
}
@Test
public void testMatcher(){
when(mockedList.get(anyInt())).thenReturn("element");
System.out.println(mockedList.get(999));
}
@Test
@DisplayName("testUsingTime")
public void testUsingTime(){
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
verify(mockedList, never()).add("never happened");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");
}
}
- 主要看一下使用mockito进行切面测试(Controller)
public class Keywords implements Serializable {
private Integer id;
private String keyword;
private String notes;
public Keywords(){}
@Override
public String toString() {
return "Keywords{" +
"id=" + id +
", keyword='" + keyword + '\'' +
", notes='" + notes + '\'' +
'}';
}
public Integer getId() {
return id;
}
public String getKeyword() {
return keyword;
}
public String getNotes() {
return notes;
}
private Keywords(Builder builder){
this.id=builder.id;
this.keyword = builder.keyword;
this.notes = builder.notes;
}
public static class Builder{
private Integer id;
private String keyword;
private String notes;
public Builder setId(Integer id) {
this.id = id;
return this;
}
public Builder setKeyword(String keyword) {
this.keyword = keyword;
return this;
}
public Builder setNotes(String notes) {
this.notes = notes;
return this;
}
public Keywords build(){
return new Keywords(this);
}
}
}
@Controller
public class KeywordController {
@Autowired
private KeywordsService keywordsService;
@Autowired
private KeywordsServiceImpl keywordsServiceImpl;
@GetMapping(value = "/api/keywords")
public Keywords findKeywordById(@RequestParam(value = "id") Integer id) {
return keywordsService.findKeywordById(id);
}
@PostMapping("/api/add")
@ResponseBody
public Boolean addOne(@RequestBody Keywords keywords){
return keywordsServiceImpl.addOne(keywords);
}
}
@Repository
public interface KeywordsService {
Keywords findKeywordById(int i);
Boolean addOne(Keywords keywords);
}
@Service
public class KeywordsServiceImpl implements KeywordsService {
@Override
public Keywords findKeywordById(int i) {
return null;
}
@Override
public Boolean addOne(Keywords keywords) {
System.out.println("invoke spy class method");
System.out.println(keywords);
return false;
}
}
public class MvcMockitoTest {
protected MockMvc mockMvc;
@Mock
private KeywordsService keywordsService;
@Spy
private KeywordsServiceImpl keywordsServiceImpl;
@InjectMocks
private KeywordController controller;
@BeforeEach()
public void setup() {
MockitoAnnotations.openMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
@Test
@DisplayName("findKeywordByIdTest")
public void findKeywordByIdTest() throws Exception {
Keywords keywords = new Builder().setId(666).setKeyword("tester").setNotes("notes").build();
Mockito.when(keywordsService.findKeywordById(1)).thenReturn(keywords);
MvcResult mvcResult = mockMvc.perform(
get("/api/keywords?id=1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print())
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}
@Test
@DisplayName("addOne")
public void testAddOne() throws Exception {
Keywords build = new Builder().setId(1).setKeyword("addOne").setNotes("testAddOne").build();
Gson gson = new Gson();
String jsonString =gson.toJson(build);
System.out.println(jsonString);
MvcResult mvcResult = mockMvc.perform(
MockMvcRequestBuilders.post("/api/add")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString)
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print()).andReturn();
int status = mvcResult.getResponse().getStatus();
assertEquals(status,200);
System.out.println("输出 " + mvcResult.getResponse().getContentAsString());
}
}
结果:
mockito可以配合junit5的断言功能使用。更多用法可以参考官方文档
|