测试好处多多。但在 spring boot 里写测试,别说得到好处,就连把测试框架搭对都不是个简单的事。
毋庸置疑, 相对于 Golang, python 的网络测试框架, java 里 spring 里的测试框架真是复杂的可以. 约定优于配置, 这约定漫天飞舞藏在文档的各个角落. 版本还不统一.
一般我们写后端逻辑分 3 层, Controller -> Service -> Repository,简单来说,
对于单元测试 ,我们只针对某一个功能点写测试
而对于集成测试 ,我们会集成多个功能。
spring boot 为这两种测试,都提供了具体的约定方法。
spring boot 启动慢,我们测试时应该尽可能的只启动我们需要的类。 怎么做到呢?
spring boot 分层测试
我们看上面 spring 简化的请求图。 如果是单元测试。我们应该只对 3 测(依赖 4),只对 4 测(依赖 5)。 而集成测试,我们应该是可以测 1 到 5. 当然,这个界定根据你的需求来。
在测试前,我们先把 pom 的包统一下.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/>
</parent>
?
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
?
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
?
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
完整源码地址:?https://github.com/zk4/spring_test_demo
测试场景
我们要将测试场景列个矩阵
不加载 spring 框架 的单元测试
这是最灵活也是最快的一种方案
拿 Controller 举例
Controller 其实就是一个类. 理论上 new 出来测就行了
但问题是: Controller 里会被 Spring 注入一堆东西.
简单的解决方法是:
不要在成员上加 @Autowired, 而是在构造时自动注入. 这也是 Spring 官方的推荐做法.
@Controller
class UserController{
//不要这样
@Autowired
UserService userService;
}
?
@Controller
class UserController{
UserService userService;
//建议这样
@Autowired
UserController(UserService userService){
this.userService = userService;
}
}
那 UserService 直接 new? UserService 也是有 Spring 注入的.
我们知道 java 里可以给类做代理.
那么,对构造函数是类的, 我们只要代理这个类,然后模拟类函数的返回值就行了.
这个过程. spring test 里的 Mokito 做了封装.
在 Test 类里, 操作如下:
//测试类的头, 使 Mock 生效
@RunWith(MockitoJUnitRunner.class)
public class UserControllerTest{
// 指定要 Mock 的类
@Mock
UserService userService;
?
@Test
public testhello(){
// Mock 的类里的函数怎么返回
given(userService.getUser(1))
.willReturn(new User().setName("bob").setId(1));
....
}
}
怎么模拟 http 请求访问呢?
Mockito 帮你做, 但不是真正的网络请求! 是模拟的. 就是不会经过网卡. 而是直接将模拟网络请求塞给 Controller.
见下面 @Test 方法.
package com.zk.controller;
?
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zk.entity.User;
import com.zk.exception.UserNotFound;
import com.zk.service.UserService;
import org.assertj.core.api.Assertions;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
?
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
//由 Junit 4 启动 Mockito
@RunWith(MockitoJUnitRunner.class)
@AutoConfigureRestDocs
public class MyControllerTest1 {
?
?
?
private MockMvc mvc;
?
@Mock
// 要 Mock 的类
UserService userService;
?
@InjectMocks
// Mock 要注入的类
UserController userController;
?
?
@Before
public void setUp() {
mvc = MockMvcBuilders.standaloneSetup(userController)
//指定 Exception 处理器
.setControllerAdvice(new UserExceptionAdvice())
//.addFilters(new UserFilter()) //你也可以指定 filter , interceptor 之类的, 看 StandaloneMockMvcBuilder 源码
.build();
}
?
@Test
public void getUserTest() throws Exception {
// given
User bob = new User().setName("bob").setId(1);
given(userService.getUser(1))
.willReturn(bob);
?
// when
MockHttpServletResponse response = mvc.perform(
get("/user/1")
.accept(MediaType.APPLICATION_JSON)
?
)
.andReturn()
.getResponse();
?
// then
ObjectMapper objectMapper = new ObjectMapper();
Assert.assertEquals(response.getStatus(), HttpStatus.OK.value());
Assert.assertEquals(response.getContentAsString(), objectMapper.writeValueAsString(bob));
}
?
?
@Test
public void getUserNotFound() throws Exception {
//given
given(userService.getUser(999))
.willThrow(new UserNotFound());
?
// when
MockHttpServletResponse response = mvc.perform(
get("/user/999")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
?
// then
Assertions.assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
Assertions.assertThat(response.getContentAsString()).isEmpty();
}
}
?
加载 spring 框架, 集成测试
这个相对来说最简单的. 也是最符合直觉的,
但就是, 太慢. 要启整个 Spring.
package com.zk.controller;
?
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zk.entity.User;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
?
?
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyControllerTest2 {
?
// 要注意, 不要用 RestTemplate,
// 因为 TestRestTemplate 在测试环境里多做了很多事,
// 比如: 帮你自己把当前 host:port 加上了. (尤其是咱们还指定了随机端口)
// 能自动加账号密码,
// ErrorHandler 被设成了 NoOpResponseErrorHandler.
// 最重要的, 能在测试类里一键注入啊...
@Autowired
TestRestTemplate restTemplate;
?
@Test
public void getUser() throws Exception {
// given
User user = new User().setName("bob").setId(1);
?
// when
ResponseEntity<User> response = restTemplate.getForEntity("/user/1", User.class);
?
?
// then
ObjectMapper objectMapper=new ObjectMapper();
?
Assert.assertEquals(response.getStatusCode(),HttpStatus.OK);
Assert.assertEquals(
objectMapper.writeValueAsString(response.getBody()),
objectMapper.writeValueAsString(user)
);
}
}
?
怎么 Mock?
假如我们要替换 UserService 的返回. 但 spring 的 interceptor, AdviceController 之类的都要正常加载咋整.
你可以按上一节的方式,也有更简单的方法.
在 @SpringBootTest 的加成下! 你可以直接使用 @AutoConfigureMockMvc
package com.zk.controller;
?
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zk.entity.User;
import com.zk.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
?
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
?
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 允许 Mock
@AutoConfigureMockMvc
public class SpringRunner_mock {
?
?
@Autowired
MockMvc mvc;
?
@MockBean
UserService userService;
?
?
@Test
public void getUser() throws Exception {
// given
User bob = new User().setName("bob").setId(1);
given(userService.getUser(1))
.willReturn(bob);
?
?
// when
MockHttpServletResponse response = mvc.perform(
get("/users/1")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
?
// then
ObjectMapper objectMapper=new ObjectMapper();
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
objectMapper.writeValueAsString(bob)
);
}
}
?
加载 spring 部分框架(仅自动加载 Controller) 单元测试
通过 @WebMvcTest 指定即可. 你也可以不写指定的 Controller.class ,那你得 Mock 所有 Controller 的依赖才行.
@WebMvcTest 这个注解干的事就多了... 你可以点开源码看一眼, 有没有熟悉的 @AutoConfigureMockMvc
package com.zk.controller;
?
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zk.entity.User;
import com.zk.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
?
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
?
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class SpringRunner_unit_controller_only {
?
@Autowired
private MockMvc mvc;
?
// 会自动注入到 controller
@MockBean
private UserService userService;
?
@Test
public void getUser() throws Exception {
// given
User bob = new User().setName("bob").setId(1);
given(userService.getUser(1))
.willReturn(bob);
?
// when
MockHttpServletResponse response = mvc.perform(
get("/users/1")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
?
// then
ObjectMapper objectMapper = new ObjectMapper();
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
objectMapper.writeValueAsString(bob)
);
}
}
|