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

引子:自上世纪末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 参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容