作者:张超超、王婉伊 编辑:毕小烦
Mock 这个词对于测试人员来说并不陌生,当我们要测试的接口 A 依赖接口 B ,可 B 无法满足我们的测试需求时,需要 Mock 一下接口 B,来测试 A。当前端和服务端并行开发时,如果服务端接口还没有开发好,前端同学也会 Mock 一下。
那 Mock 到底是什么?
维基百科:
在面向对象程序设计中,模拟对象(英语:mock object,也译作模仿对象)是以可控的方式模拟真实对象行为的假的对象。程序员通常创造模拟对象来测试其他对象的行为,很类似汽车设计者使用碰撞测试假人来模拟车辆碰撞中人的动态行为。
注意这里的关键信息,以可控的方式,模拟真实对象行为,假的对象。
为什么要模拟呢?
一定是因为没有办法或者不需要用真实的服务,比如:
- 真实的对象还没开发好,但你又急需测试;
- 真实的对象是第三方的(比如各种开放平台),没有提供联调环境,或是不便联调,或是搭建很麻烦;
- 真实的对象无法覆盖你要的测试场景(比如网络错误),而使用 Mock,你想要什么就可以模拟什么;
- 真实的对象速度很慢,而模拟的非常快;
- 等等。
这些情况下,模拟对象是一个非常好的解决方案,它可以让你的测试不被阻断,还能模拟出你想要的各种场景。
但模拟,不是真实,模拟是按照我们设定的剧本在走,是我们自己控制的,而真实的情况可能存在各种不确定性,所以不能完全相信 Mock。另外,Mock 是把真实对象模拟了一遍,如果真实对象改了,模拟对象也得跟着改,数量一多,维护起来也是十分的麻烦。
怎么 Mock ?
本文介绍我们团队在实际的项目中遇到的 3 种 Mock 场景,以及我们的设计方案。
案例1. Mock HTTP:通过域名映射实现 Mock
项目背景
我们要测试一个 HTTP 接口,这个接口在服务内部的处理会因地区不同而存在差异,虽然对外提供的业务接口只有 1 个,但在服务内部,如果判断是 A 地区就会去调用 A 地区的接口,B 地区会去调用 B 地区的接口,不同地区通过域名区分。
比如,要测的是登录接口 test.com/login,到了内部处理时就会因不同的地区去调用不同接口:
- A 地区会调用:test.a.com/login
- B 地区会调用:test.b.com/login
- C 地区会调用:test.c.com/login
- ……
如下所示:
为了便于测试,我们需要 Mock 各个地区的接口,应该怎么做呢?
设计方案
方案如下:
在「中台」所在的服务器上,将调用 A 地区接口的域名通过 hosts 中的配置映射到 Mock 平台的 IP。配置好以后,「中台」调用 A 地区的接口时,请求都会转发到 Mock 服务器上,然后我们就可以在 Mock 服务器上对具体的接口进行配置,定制返回信息;
需要 Mock 多个地区时,将对应域名加到 hosts 里映射即可。当然也可以 Mock 一个固定的域名,如 test.mock.com,然后每个地区的域名进行配置化(配置中心)。当我们要 Mock 某个地区时,只需将该地区的配置的域名改为 test.mock.com 即可,这样就不用去修改 hosts 文件了。
这种方案的优点是:
- 没有代码侵入,域名可通过 hosts 进行配置,将不同的域名映射到 Mock 服务器;
- 配置简单,没有什么门槛;
缺点是:
- 依赖的数据需要我们自己来配置,这需要额外投入精力去研究被依赖服务的接口信息,还要维护 Mock 数据;
- Mock 控制粒度比较粗。在无任何代码侵入的前提下,比如在测试 A 地区的接口时,需将被调用的所有接口都进行 Mock,而不能只 Mock 其中几个接口。
- 仅支持 Mock HTTP 请求。
案例2. Mock RPC:基于 AOP 实现 Mock
项目背景
这个项目对另外一个项目有强依赖,并且我们对这个 Mock 方案的要求是,除了能满足我们自己的常规测试外,还要能提供给外部客户进行快速对接、联调,要能支持 HTTP 和 RPC(Dubbo)。设计方案
为了满足这个需求,我们开发了 Mock SDK。
SDK 中包含几个拦截器:
AbstractAspect
类:是一个抽象切面拦截器,是其它拦截器的父类,提供了一些抽象方法让子类实现,以及一些通用的方法。ControllerAspect
类:HTTP 接口拦截器,负责获取接口的 URL、获取全部请求头、获取全部请求参数、获取全部请求体数据。FacadeAspect
类:Dubbo 接口拦截器,负责获取方法名及其参数。AnnotationsAspect
类:注解切面拦截器,当 Java 类不在 client 下,但是需要 Mock 对应的方法时,可以在该方法前加上注解@Mock
。
使用时只需:
- 在
pom
文件中引入 Mock SDK; spring-scan-bean
配置 Mock 的工具类,加载到 spring 上下文容器中;- 设置 Mock 开关配置。开启 Mock 模式时,默认会对所有
controller
中的接口、client
中的 Dubbo 方法以及所有加了 Mock 注解的方法进行拦截。
方案如下:
这种方案的优点是:
- 支持 Mock HTTP,提供给客户联调测试时可在
controller
层进行 Mock,但不推荐这么做,客户自己 Mock HTTP 接口会更灵活; - 支持 Mock RPC,我们自己测试时可在
client
层进行 Mock; - Mock 粒度更细:支持按接口粒度进行 Mock,还支持单个 Java 方法添加注解来实现 Mock;
- Mock 节点灵活:
controller
层、client
层、或加了 Mock 注解的 Java 方法均可。
缺点是:
- 性能问题:在 Mock 开启模式下,每次请求都会去判断是否存在 Mock 对象,接口性能会有一定程度受影响。
- Dubbo 接口目前只在
client
层做的切面,所以在 Mock 平台配置返回值时,出参字段没办法从接口文档中直接获取,因为client
层的出参字段与接口文档的参数字段不完全一致。 - 存在代码侵入。就算在 Mock 配置为关闭的情况下,仍会生成一个无逻辑的切面。
案例3. Mock Python:基于数据存储中间件实现 Mock
项目背景
该项目也是需要对接多个区域,不同区域的接口存在很大差异,而且被测服务的末端是 Python 服务。另外一个重要的点是:该项目的测试数据非常有限且不可重复利用。
其实用「方案1」去 Mock 接口也是可以的,但:
- 需要花很多时间去预研 Mock 数据;
- 被依赖服务的接口加解密方式、调用链路差异很大;
- 配置成本过高:比如一个登录功能,有些区域可能需要调 5 个接口,有些可能要 7 个接口才能实现;为了实现一个业务功能,往往需要配几十个接口的 Mock。
因此在权衡了时间、资源、风险利弊之后,我们采取了新的 Mock 方案 —— 基于数据存储中间件的 Mock,我们用的是 Redis。
设计方案
基于数据存储中间件的 Mock 方案其实是在被测服务中加入 Mock 逻辑,当启用 Mock 时,直接从 Redis 获取数据,而不去请求真实的数据,这就实现了 Mock 的目的。
Mock 方法:
- 配置 Mock 开关,便于开启和关闭;
- 在 Redis 中配置 Mock 对应的
Key/Value
;
优点:
- 无需额外搭建 Mock 平台,研发投入成本低;
- 无需关注各区域接口差异,Mock 的数据格式统一;
- 配置和维护成本低,也可编写脚本实现批量 Mock。
缺点:
- 业务代码验证不全,Python 服务的业务代码是验证不到的。所以,Mock 测试之后,还要用仅存的真实数据去验证一遍。
- 存在代码侵入。为实现 Mock 功能,存在非业务性逻辑。
总结
上述这 3 个案例就是我们在实际项目中遇到并实践过的 Mock 方案,当然还有其他的方案,这需要结合项目的实际情况综合评估风险、资源、利弊后再做选择。本文只是提供了一些思路,希望对大家有所启发。
(完)