面向开发的测试技术(一):Mock

4,139 阅读7分钟

引子:自上世纪末Kent Beck提出TDD(Test-Driven Development)开发理念以来,开发和测试的边界变的越来越模糊,从原本上下游的依赖关系,逐步演变成你中有我、我中有你的互赖关系,甚至很多公司设立了新的QE(Quality Engineer)职位。和传统的QA(Quality Assurance)不同,QE的主要职责是通过工程化的手段保证项目质量,这些手段包括但不仅限于编写单元测试、集成测试,搭建自动化测试流程,设计性能测试等。可以说,QE身上兼具了QA的质量意识和开发的工程能力。从这篇开始,我会从开发的角度分三期聊聊QE这个亦测试亦开发的角色所需的基本技能。

1 什么是Mock?

在软件测试领域,Mock的意思是模拟,简单来说,就是通过某种技术手段模拟测试对象的行为,返回预先设计的结果。这里的关键词是预先设计,也就是说对于任意被测试的对象,可以根据具体测试场景的需要,返回特定的结果。打个比方,就像BBC纪录片里面的假企鹅,可以根据拍摄需要作出不同的反应。

2 Mock有什么用?

理解了什么是Mock,再来看Mock有哪些用途。首先,Mock可以用来解除测试对象对外部服务的依赖(比如数据库,第三方接口等),使得测试用例可以独立运行。不管是传统的单体应用,还是现在流行的微服务,这点都特别重要,因为任何外部依赖的存在都会极大的限制测试用例的可迁移性和稳定性。可迁移性是指,如果要在一个新的测试环境中运行相同的测试用例,那么除了要保证测试对象自身能够正常运行,还要保证所有依赖的外部服务也能够被正常调用。稳定性是指,如果外部服务不可用,那么测试用例也可能会失败。通过Mock去除外部依赖之后,不管是测试用例的可迁移性还是稳定性,都能够上一个台阶。

Mock的第二个好处是替换外部服务调用,提升测试用例的运行速度。任何外部服务调用至少是跨进程级别的消耗,甚至是跨系统、跨网络的消耗,而Mock可以把消耗降低到进程内。比如原来一次秒级的网络请求,通过Mock可以降至毫秒级,整整3个数量级的差别。

Mock的第三个好处是提升测试效率。这里说的测试效率有两层含义。第一层含义是单位时间运行的测试用例数,这是运行速度提升带来的直接好处。而第二层含义是一个QE单位时间创建的测试用例数。如何理解这第二层含义呢?以单体应用为例,随着业务复杂度的上升,为了运行一个测试用例可能需要准备很多测试数据,与此同时还要尽量保证多个测试用例之间的测试数据互不干扰。为了做到这一点,QE往往需要花费大量的时间来维护一套可运行的测试数据。有了Mock之后,由于去除了测试用例之间共享的数据库依赖,QE就可以针对每一个或者每一组测试用例设计一套独立的测试数据,从而很容易的做到不同测试用例之间的数据隔离性。而对于微服务,由于一个微服务可能级联依赖很多其他的微服务,运行一个测试用例甚至需要跨系统准备一套测试数据,如果没有Mock,基本上可以说是不可能的。因此,不管是单体应用还是微服务,有了Mock之后,QE就可以省去大量的准备测试数据的时间,专注于测试用例本身,自然也就提升了单人的测试效率。

3 如何Mock?

说了这么多Mock的好处,那么究竟如何在测试中使用Mock呢?针对不同的测试场景,可以选择不同的Mock框架。

3.1 Mockito

如果测试对象是一个方法,尤其是涉及数据库操作的方法,那么Mockito可能是最好的选择。作为使用最广泛的Mock框架,Mockito出于EasyMock而胜于EasyMock,乃至被默认集成进Spring Testing。其实现原理是,通过CGLib在运行时为每一个被Mock的类或者对象动态生成一个代理对象,返回预先设计的结果。集成Mockito的基本步骤是:

  1. 标记被Mock的类或者对象,生成代理对象
  2. 通过Mockito API定制代理对象的行为
  3. 调用代理对象的方法,获得预先设计的结果

下面是我GitHub上的示例工程里的一个例子,

@RunWith(SpringRunner.class)
@SpringBootTest
public class SignonServiceTests {

    // 测试对象,一个服务类
    @Autowired
    private SignonService signonService;

    // 被Mock的类,被服务类所依赖的一个DAO类
    @MockBean
    private SignonDao dao;

    @Test
    public void testFindAll() {
        // SignonService#findAll()内部会调用SignonDao#findAll()
        // 如果不做定制,所有被Mock的类默认返回空
        List<Signon> signons = signonService.findAll();
        assertTrue(CollectionUtils.isEmpty(signons));

        // 定制返回结果
        Signon signon = new Signon();
        signon.setUsername("foo");
        when(dao.findAll()).thenReturn(Lists.newArrayList(signon));

        signons = signonService.findAll();
        // 验证返回结果和预先设计的结果一致
        assertEquals(1, signons.size());
        assertEquals("foo", signons.get(0).getUsername());
    }
}

从上面的测试用例可以看到,通过Mock服务类所依赖的DAO类,我们可以跳过所有的数据库操作,任意定制返回结果,从而专注于测试服务类内部的业务逻辑。这是传统的非Mock测试所难以实现的。

注意:Mockito不支持Mock私有方法或者静态方法,如果要Mock这类方法,可以使用PowerMock

3.2 WireMock

如果说Mocketo是瑞士军刀,可以Mock Everything,那么WireMock就是为微服务而生的倚天剑。和处在对象层的Mockito不同,WireMock针对的是API。假设有两个微服务,Service-A和Service-B,Service-A里的一个API(姑且称为API-1),依赖于Service-B,那么使用传统的测试方法,测试API-1时必然需要同时启动Service-B。如果使用WireMock,那么就可以在Service-A端Mock所有依赖的Service-B的API,从而去掉Service-B这个外部依赖。

同样看一个我GitHub上的示例工程里的一个例子,

@RunWith(SpringRunner.class)
@WebMvcTest(VacationController.class)
public class VacationControllerTests {

    // Mock被依赖的另一个微服务
    @Rule
    public WireMockRule wireMockRule = new WireMockRule(3001);

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Before
    public void before() throws JsonProcessingException {
        // 定制返回结果
        JsonResult<Boolean> expected = JsonResult.ok(true);
        stubFor(get(urlPathEqualTo("/api/vacation/isWeekend"))
                .willReturn(aResponse()
                        .withStatus(OK.value())
                        .withHeader(CONTENT_TYPE, APPLICATION_JSON_UTF8_VALUE)
                        .withBody(objectMapper.writeValueAsString(expected))));
    }

    @Test
    public void testIsWeekendProxy() throws Exception {
        // 构造请求参数
        VacationRequest request = new VacationRequest();
        request.setType(PERSONAL);
        OffsetDateTime lastSunday = OffsetDateTime.now().with(TemporalAdjusters.previous(SUNDAY));
        request.setStart(lastSunday);
        request.setEnd(lastSunday.plusDays(1));

        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/vacation/isWeekend");
        request.toMap().forEach((k, v) -> builder.param(k, v));
        JsonResult<Boolean> expected = JsonResult.ok(true);

        mockMvc.perform(builder)
                // 验证返回结果和预先设计的结果一致
                .andExpect(status().isOk())
                .andExpect(content().contentType(APPLICATION_JSON_UTF8))
                .andExpect(content().string(objectMapper.writeValueAsString(expected)));
    }
}

和Mockito类似,在测试用例中集成WireMock的基本步骤是:

  1. 声明代理服务,以替代被Mock的微服务
  2. 通过WireMock API定制代理服务的返回结果
  3. 调用代理服务,获得预先设计的结果

值得一提的是,除了API方式的集成,WireMock还支持以Jar包的形式独立运行,从配置文件中加载预先设计的响应结果,以替代被Mock的微服务。更多信息可以参阅官方文档

其他类似的Mock API的框架还有OkHttp的mockwebservermocomockserver。mockwebserver也属于嵌入式Mock框架的范畴,但功能过于简单。moco,mockserver虽然功能完善,但需要独立部署,和WireMock相比不具有优势。

4 小结

以上就是我对Mock技术的一些见解,欢迎你到我的留言板分享,和大家一起过过招。最后还要说一句,Mock技术虽然强大,但主要还是适用于单元测试,在集成测试,性能测试,自动化测试等其他测试领域使用并不多。

5 参考