最近在写一个微服务的项目,项目中使用了Mock。Mock 方法是单元测试中常见的一种技术,它的主要作用是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。
定义
1.什么是Mock
mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为。比如说你需要调用B服务,可是B服务还没有开发完成,那么你就可以将调用B服务的那部分给Mock掉,并编写你想要的返回结果。
2.Spring Boot的测试类库
现在绝大多数的java服务都是Spring框架搭建的,并且也会使用到Spring boot来进行快速搭建开发,在Spring Boot提供了许多实用工具和注解来帮助测试应用程序,主要包括以下两个模块:
- spring-boot-test:支持测试的核心内容。
- spring-boot-test-autoconfigure:支持测试的自动化配置。
开发进行只要使用 spring-boot-starter-test 启动器就能引入这些 Spring Boot 测试模块,还能引入一些像 JUnit, AssertJ, Hamcrest 及其他一些有用的类库。
3.Mockito工具
Mockito 是 Java 单元测试 Mock 框架,开源。
大多 Java Mock 库如 EasyMock 或 JMock 都是 expect-run-verify (期望-运行-验证)方式,而 Mockito 则使用更简单,更直观的方法:在执行后的互动中提问。使用 Mockito,你可以验证任何你想要的。而那些使用 expect-run-verify 方式的库,你常常被迫查看无关的交互。
非 expect-run-verify 方式 也意味着,Mockito 无需准备昂贵的前期启动。他们的目标是让开发人员专注于测试选定的行为。
Mockito 拥有的非常少的 API,所有开始使用 Mockito,几乎没有时间成本。因为只有一种创造 mock 的方式。只要记住,在执行前 stub,而后在交互中验证。你很快就会发现这样的TDD java 代码是多么自然。
类似 EasyMock 的语法来的,所以你可以放心地重构。Mockito 并不需要“expectation(期望)”的概念。只有 stub 和verify。
Mockito 实现了 Gerard Meszaros 所谓的 Test Spy.
项目中,有些函数需要处理某个服务的返回结果,而在对函数单元测试的时候,又不能启动那些服务,这里就可以利用Mockito工具,其中有如下三种注解:
-
@InjectMocks:创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
-
@Mock:对函数的调用均执行mock(即虚假函数),不执行真正部分。
-
@Spy:对函数的调用均执行真正部分。
Mock 与 Stub 的区别
Mock 不是 Stub,两者是有区别的:
前者被称为 mockist TDD,而后者一般称为 classic TDD ;
前者是基于行为的验证(behavior verification),后者是基于状态的验证 (state verification);
前者使用的是模拟的对象,而后者使用的是真实的对象。
所谓打桩Stub,就是用来提供测试时所需要的测试数据,因为是mock的对象,所以可能有些方法并不能知道返回值,因此我们需要去假定返回值。可以对各种交互设置相应的回应,即对方法设置调用返回值,使用when(…).thenReturn(…)。
4. 常用的 Mockito 方法:
Mockito的使用,一般有以下几种组合:
-
do/when:包括doThrow(…).when(…)/doReturn(…).when(…)/doAnswer(…).when(…)
-
given/will:包括
given(…).willReturn(…)/given(…).willAnswer(…)
例如:given(userRepository.findByUserName(Mockito.anyString())).willReturn(user);
given用于对指定方法进行返回值的定制,它需要与will开头的方法一起使用
通过willReturn可以直接指定打桩的方法的返回值 -
when/then: 包括when(…).thenReturn(…)/when(…).thenAnswer(…)
例如:when(userRepository.findByUserName(Mockito.anyString())).thenReturn(user);
when的作用与Given有点类似,但它一般与then开头的方法一起使用。
thenReturn与willReturn类似,不过它一般与when一起使用。
具体应用
以下一个具体例子:
/**
* If an item is loaded from the repository, the name of that item should
* be transformed into uppercase.
*/
@Test
public void shouldReturnItemNameInUpperCase() {
//
// Given
//
Item mockedItem = new Item("it1", "Item 1", "This is item 1", 2000, true);
when(itemRepository.findById("it1")).thenReturn(mockedItem);
//
// When
//
String result = itemService.getItemNameUpperCase("it1");
//
// Then
//
verify(itemRepository, times(1)).findById("it1");
assertThat(result, is("ITEM 1"));
不同代码块的作用显而易见。不过我们还是总结下吧。
准备数据->Given
这个部分,创建我们Mock的函数的返回值,或者我们将要测试方法的输入参数。此外,mock的方法也会在这个部分中准备。通常单元测试Case中,这个部分应该是最长,也是最复杂的。
注意:Mockito的when声明其实是given部分的,这点需要说明下,因为的确容易困惑。但,这与测试执行的准备工作有关,因此,放在这里最为合适。
执行->When
这里一般只Call测试方法,这里标明了测试目的,因为这个部分的代码一般是最短的了。
验证->Then
这个部分,执行环节的所有结果在这里得以声明。除此之外,也可以确认方法是否被执行。总之,主要的点,在这里进行Check。
测试Case的命名(测试方法)
早期测试方法都是以test为前缀,现在已经不怎么这样做了,一些同事喜欢用下划线去命名,我个人喜欢用驼峰命名。
当然,一个方法命名需要包含大量的信息,但可能这些信息更好的方式是放在测试代码的注释上。表明这个方法会发生什么,像shouldReturnItemNameInUpperCase ()这样,可能是个好的办法,显而易见,在项目开始前,大家统一测试方法规范是好的。
Controller层的例子:
Controller层的单元测试比较简单,主要思路是mock业务层返回值,然后模拟调用接口。
Controller.java
@RestController
public class Controller {
@Autowired
private final IndiskService indiskService;
private static final String AUTHORIZATION = "Authorization";
@PostMapping("/ebs/{version}/volumes/{volume_id}/action/attach")
public ReturnMsgVO attach(@PathVariable("volume_id") String volume_id,
@RequestBody JSONObject jsonObject,
HttpServletRequest request) {
String keycloakToken = request.getHeader(AUTHORIZATION);
AttachDetachExtendVO attachDetachExtendVO = new AttachDetachExtendVO();
//do something......
//正确返回200
return indiskService.attachVolume(attachDetachExtendVO, request, keycloakToken);
}
@DeleteMapping("/ebs/{version}/volumes/{volume_id}/soft")
public ReturnMsgVO softDeleteVolume(@PathVariable(value = "volume_id") String volumeId,
@RequestBody String orderJson,
HttpServletRequest request) {
String keycloakToken = request.getHeader(AUTHORIZATION);
//正确返回202,消息体为{"code":"202","message":null,"data":null}
return indiskService.softDeleteVolume(volumeId, keycloakToken, orderJson);
}
}
测试类
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public class ControllerTest {
private static final String AUTHORIZATION = "Authorization";
private MockMvc mvc;
@Mock
//要mock被测类中依赖的对象使用@Mock注解
private ServiceImpl serviceImpl;
@InjectMocks
//被测类本身使用@InjectMocks注解
private Controller controller;
@Before
public void setup() {
//初始化
MockitoAnnotations.initMocks(this);
//构建mvc环境
mvc = MockMvcBuilders.standaloneSetup(controller).build();
}
@Test
public void testSoftDeleteVolume() throws Exception {
//mock方法行为
Mockito.when(serviceImpl.softDeleteVolume("1", "2", "3"))
.thenReturn(ReturnMsgVO.builder().code("202").build());
//模拟接口调用
this.mvc.perform(delete("/ebs/v1/volumes/1/soft")
.header(AUTHORIZATION, "2").content("3"))
.andExpect(status().isOk())
//对接口响应进行验证
.andExpect(content().json("{\"code\":\"202\",\"message\":null,\"data\":null}"));
}
@Test
public void attach() throws Exception {
Mockito.when(serviceImpl.attachVolume(Mockito.any(), Mockito.any(), Mockito.anyString()))
.thenReturn(ReturnMsgUtil.success());
mvc.perform(post("/ebs/v1/volumes/123/action/attach")
.header(AUTHORIZATION, "123")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"os-attach\":{\"instance_uuid\":\"123\"}}"))
.andExpect(status().isOk());
}
}
总结
为了去正确执行一个测试,有时为此准备大量的数据,是非常头疼的。尤其是如果在不同的测试Case,我们需要去Mock相同的一些方法或者数据,那么这个时候,我们做个类函数,去共享这些,这样会比较好,当然这些Mock对象因为影响着诸多的测试,所以也会越变越复杂,而且大量的测试这些对象。因此,在创建测试,Mock数据时,怎么做,如何做,我们需要权衡利弊,再三考虑。