【spring】 在使用 spring 的场景进行单元测试

说明如何用 spring 进行单元测试

前言

spring mvc mock 测试

基于 rest 的 spring mvc 的测试,可以测试完整的 spring mvc 流程,即从 url 请求到控制器处理,再到视图渲染都可以测试。MockMvcBuilder 是用来构造 mock mvc 的构造器,其主要有两个实现:StandaloneMockMvcBuilder 和 DefaultMockMvcBuilder,分别对应独立安装和集成 web 测试。独立安装测试会启动一个 web 服务器,而集成 web 测试通过相应的 mock api 进行模拟测试,不会启动服务器,一般使用集成 web 测试就可以了。

集成 web 环境方式示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = App.class)
@WebAppConfiguration
public class ControllerTest {

@Autowired
private WebApplicationContext wac; // 获取 web application context

private MockMvc mockMvc;

@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); //构造 mock mvc
}

@Test
public void testHello() throws Exception {
mockMvc.perform(get("/hello")) // 模拟 http 请求
.andExpect(status().isOk()); // 断言测试
}
}

说明

MockMvc.perform 方法用于执行测试,perform 方法接收一个 RequestBuilder 参数,返回 ResultActions

RequestBuilder 可以由 MockMvcRequestBuilders 中的 get, post 等静态工厂方法产生,它们与 http method 一一对应。 get, post 等方法返回 MockHttpServletRequestBuilder, MockHttpServletRequestBuilder 提供方法用于修改 http 请求,例如设置 header,body 等

ResultActions 提供 3 个操作:

  • ResultActions andExpect(ResultMatcher matcher):添加断言测试结果是否符合预期
    ResultMatcher 用来做响应结果验证,可以用 MockMvcResultMatchers 中的静态工厂方法产生具体实例
  • ResultActions andDo(ResultHandler handler):添加结果处理器,用于对验证成功后执行的动作
    ResultHandler 是结果处理,由用户自定义
  • MvcResult andReturn():获取返回的结果,可用于进一步的测试

示例

  1. 测试普通控制器
1
2
3
4
5
6
mockMvc.perform(get("/user/{id}", 1))         // 执行请求
.andExpect(model().attributeExists("user")) // 验证 user 属性是否存在
.andExpect(view().name("user/view")) // 验证 viewName
.andExpect(forwardedUrl("/WEB-INF/jsp/user/userView.jsp")) // 验证视图渲染时 forward 到的 jsp
.andExpect(status().isOk()) // 验证 http 状态码
.andDo(print()); // 打印 MvcResult
  1. 用得到 MvcResult 进行自定义验证
1
2
3
4
MvcResult result = mockMvc.perform(get("/user/{id}", 1)) // 执行请求  
.andReturn(); //返回 MvcResult

Assert.assertNotNull(result.getModelAndView().getModel().get("user")); // 用 JUnit 自定义的断言
  1. 文件上传
1
2
3
4
byte[] bytes = new byte[] {1, 2};  
mockMvc.perform(fileUpload("/user/{id}/icon", 1L).file("icon", bytes)) //执行文件上传
.andExpect(model().attribute("icon", bytes)) // 验证属性相等性
.andExpect(view().name("success")); // 验证视图
  1. 异步测试
1
2
3
4
5
6
7
8
9
10
MvcResult result = mockMvc.perform(get("/user/async1?id=1&name=zhang"))  // 执行请求  
.andExpect(request().asyncStarted()) // 开始异步
.andExpect(request().asyncResult(CoreMatchers.instanceOf(User.class))) // 默认会等10秒超时
.andReturn();

// 异步验证
mockMvc.perform(asyncDispatch(result))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 验证响应的 content-type
.andExpect(jsonPath("$.id").value(1));

spring 中使用 mockito

mockito 是 mocking 框架。在写单元测试的过程中,可能遇到要测试的类有很多依赖,这些依赖的类/对象/资源又有别的依赖,从而形成一个大的依赖树,mockito 就是用来模拟依赖,从而简化测试的编写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

// controller 类,依赖 service 类
@RestController
public class Controller {
@Autowired
private HelloService service;

@GetMapping("/hello")
public String hello() {
return service.hello();
}
}

// service 类, hello 方法返回 "hello"
@Service
public class HelloService {
public String hello() {
return "hello";
}
}

// 测试类
import static org.mockito.Mockito.when;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = App.class)
@WebAppConfiguration
public class ControllerTest {
// 获取 web application context
@Autowired
private WebApplicationContext wac;

// 获取 controller,用来修改 controller 这个 bean 的成员
@Autowired
private Controller controller;

// 获取 service,用来在测试完后做还原(可选)
@Autowired
private HelloService service;

// HelloService 的模拟对象,用来代替真正的 service
@Mock
private HelloService mockService;

// spring mvc mock, 模拟 spring mvc 测试
private MockMvc mockMvc;

@Before
public void setup() {
// 初始化 @Mock 的对象
MockitoAnnotations.initMocks(this);
// 修改 controller 这个 bean 的成员为 mockService
// 实际使用中,要注意 controller 是否被增强后的代理对象
ReflectionTestUtils.setField(controller, "service", mockService);

this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); // 构造 mock mvc
}

// 清理,将 controller 的成员还原,方法之后的测试
@After
public void clean() {
ReflectionTestUtils.setField(controller, "service", service);
}

@Test
public void testHello() throws Exception {
// 定义 mock 对象的行为, 这里是当 mockService 的 hello 方法被调用时,返回字符串 "mock"
when(mockService.hello()).thenReturn("mock");

// 执行测试,/hello 返回 "mock"
mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string("mock"));
}
}