God is a mock. – Just Kidding
目录 Table of Contents
背景
最近接到了新任务——希望 mock 掉项目依赖的底座,主要基于两点考量:
- 内部联调时可以屏蔽掉底座的影响,不至于阻塞当前开发模块的流程;
- 之后可集成自动化测试,降低测试成本。
我的第一感觉是这个东西不好搞。一是上下游的依赖关系比较复杂,这意味着:首先让 mock 做到可以替换原来的底座以满足基本功能就需要一些努力,其次项目“看起来”运行正常并不能保证上游亦是如此;二是替换底座包含了 mock 掉数据返回和状态管理两个概念,那么就不得不在简洁和可用之间做取舍了。
本文接下来要讨论的内容,暂不考虑上游影响,并坚持这样的原则:整个模块应当是轻量的,仅对高优先级且不满足的业务场景做出适配。
业务分析
project service
- api:同步的项目入口,直接发请求或开异步任务来调用底座
- job:异步的后台任务,监控底座资源状态
- db:存储项目所需的数据
infrastructure base
- 提供核心功能的底座
external services
- 出于其它业务需求,会存储底座的某些数据
根据上图的分析,需考虑以下几点:
mock 可以代替 infrastructure base 正常响应;
project service / infrastructure bases / external services 三者之间的数据应当保持一致性;
异步的后台任务需要监控状态变化,保存状态是必需的。
架构演化
固定数据
问题描述:mock 从无到有
解决方法:选用预研的框架,可以方便地管理路由转发规则和模拟返回数据
- mock api proxy:设置路由转发规则。应该采用正则匹配,保证增删查改的通用性,预研框架支持:
- 请求路径正则匹配
- 请求参数正则匹配
- 请求主体正则匹配
- hardcoded response:提供对应路由的返回数据。应该返回全量的响应字段,其中一些字段可以灵活处理:
- UUID:创建资源时返回随机 UUID;查询/修改/删除单个资源时从路径读 UUID;查询资源列表时返回固定条目和 UUID
- 其它字段:必传字段从请求中接收;选传字段硬编码其内容;如果出现不同响应格式,路由转发规则要做区分
引入存根
问题描述:需要对返回字段做逻辑处理,固定数据模式无法直接满足
解决方法:预研框架支持存根注入,保留了扩展业务逻辑的能力
easy stub:支持简易逻辑扩展,主要可以用于:
- 提供配置
- 提取、组合和处理请求的数据
- 返回随机数据或当前时间等
注:在预研框架中,存根的入口是唯一的,因此如果希望复用多个存根,方式不太优雅。
存储数据
问题描述:目前的架构中没有办法保存数据,无法实现保持数据的一致性和监控状态变化
解决办法:引入 MVC 设计和轻量级的数据库 SQLite
- service stub:MVC 中的 Controller
- dao:MVC 中的 Model
- sqlite:轻量级关系型数据库,支持单文件和内存两种存储模式
参数匹配
问题描述:预研框架对通过请求参数匹配的支持较弱,infrastructure base A 没有相关需求,infrastructure base B 需解决该问题
解决办法:只能引入轻量的 Web 框架来补充
query matcher:强化请求参数匹配规则的核心中间件/装饰器
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
31class MatchingMode(object):
Has = "has" # 模糊匹配
Is = "is" # 精确匹配
class QueryMatcher(object):
def __init__(self, matching_rules, query_arguments, matching_mode):
self.matching_rules = matching_rules
self.query_arguments = query_arguments
self.matching_mode = matching_mode
def match(self):
# 检查请求参数的数量
if self.matching_mode == MatchingMode.Is:
if len(self.matching_rules) != len(self.query_arguments):
logger.debug("match failed: length of query arguments is not equal to length of the matching rule")
return False
# 检查请求参数的规则
for key in self.matching_rules:
if key in self.query_arguments:
rule = self.matching_rules[key]
val = str(self.query_arguments[key][0])
if not re.match(rule, val):
logger.debug("match failed: query argument not match matching rule. rule: %s, val: %s" % (rule, val))
return False
else:
logger.debug("match failed: query argument not in matching rules")
return False
# 均通过则返回真
return True
统一配置
问题描述:由于同时存在两套框架及一些业务数据,配置比较分散
解决办法:将这些配置统一到一个文件,并在文档上说明约束是有必要的
config:统一的配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
"host": "127.0.0.1",
"mock_port": 1024,
"web_port": 2333,
"db_name": "test.db",
"mock_config_path": "etc/mock_config.yml", // 其中还可以配置日志的路径
"web_config_path": "etc/web_config.json", // 其中还可以配置日志的路径
"business_field_1": "",
"business_field_2": "",
"business_field_3": "",
...
}
日志记录
问题描述:预研框架自带日志记录,但是新引入的 Web 框架没有
解决办法:需要自行补充日志模块
log:补充的日志模块
1
2
3
4
5...
[formatter_web]
format=%(asctime)s|%(name)s|%(levelname)s|%(filename)s|%(funcName)s|%(message)s
datefmt=
...
健康检查
问题描述:部署 mock 后不好快速简单地验证是否可用
解决办法:额外增加两个专门用于健康检查的接口
h-c:健康检查接口(Health-Check API),可参考的设计:
1
2
3curl --location --request GET 'http://${ip}:{port}/mock/ping' # Expected response body: {"msg": "pong"}
curl --location --request GET 'http://${ip}:{port}/web/ping' # Expected response body: {"msg": "pong"}
预告
下期将继续讨论以下这些内容:
- 技术选型
- 接口文档 / 接口测试
- 进程部署 / 容器部署
- 代办事项