4、断言库构建:断言函数封装、断言参数化、断言组合模式、断言工厂模式
说实话,很多团队写断言都是「脚踩西瓜皮」—— 想到哪写到哪。我见过一个项目里,同样的「列表不为空」判断,出现了七八种写法。维护起来?别提了,改个字段名能改到吐血。所以这一章,咱们聊聊怎么系统性地构建断言库。
我个人习惯把断言库当成一个独立的小框架来设计。它不该只是散落的函数,而是一套有层次、可组合、能扩展的体系。下面我会从四个核心模式展开,这些都是我在真实项目中反复打磨过的。
4.1 断言函数封装 —— 把脏活收起来
断言函数封装,说白了就是把那些又臭又长的判断逻辑藏进一个函数里。比如你经常要检查一个 JSON 响应里的某个字段是否在预期范围内,每次都写 assert response['data']['count'] > 0 ?太累了。
我习惯这样封装:
def assert_field_greater_than(response, field_path, threshold):
"""从嵌套字典中提取字段并断言大于阈值"""
keys = field_path.split('.')
value = response
for k in keys:
value = value[k]
assert value > threshold, f"期望 {field_path} > {threshold},实际 {value}"
你看,调用方只需要写 assert_field_greater_than(resp, 'data.count', 0)。干净、直观。我在项目中遇到过最夸张的情况:一个接口返回 30 多个字段,每个都要断言。封装之后,测试代码从 200 行缩到了 40 行。
assert_equal 就只比较相等,不要在里面偷偷打日志。单一职责,后面才好组合。
4.2 断言参数化 —— 一套逻辑,多组数据
你想想看,很多时候断言逻辑是一样的,只是数据不同。比如验证用户列表的返回格式:分页参数不同,但断言结构完全一样。这时候参数化就派上用场了。
我一般会写一个参数化装饰器,或者直接用 pytest.mark.parametrize。但如果你不想依赖框架,也可以自己实现:
def assert_paginated_response(response, expected_page, expected_size):
assert response['page'] == expected_page
assert response['size'] == expected_size
assert len(response['items']) <= expected_size
# 参数化调用
test_cases = [
({"page": 1, "size": 10, "items": [1,2]}, 1, 10),
({"page": 2, "size": 5, "items": [3]}, 2, 5),
]
for resp, page, size in test_cases:
assert_paginated_response(resp, page, size)
嗯,这里要注意:参数化不是把数据硬编码在测试里,而是把「变化的数据」和「不变的逻辑」分离。我曾经在一个支付项目里,用参数化覆盖了 20 多种金额边界场景,代码量反而减少了 60%。
4.3 断言组合模式 —— 搭积木一样写断言
组合模式是我个人最喜欢的。它让你能把多个小断言拼成一个复合断言。比如「用户注册成功」这个场景,你可能要同时检查状态码、响应体结构、数据库记录。用组合模式:
class AssertionComposite:
def __init__(self):
self._assertions = []
def add(self, assertion_callable):
self._assertions.append(assertion_callable)
def execute(self, *args, **kwargs):
for assertion in self._assertions:
assertion(*args, **kwargs)
# 使用
composite = AssertionComposite()
composite.add(lambda resp: assert resp['code'] == 200)
composite.add(lambda resp: assert 'userId' in resp['data'])
composite.add(lambda resp: assert resp['data']['role'] == 'user')
composite.execute(response)
说白了,这就是策略模式 + 组合。你可以动态地往里面加断言,甚至根据测试环境决定执行哪些。我在微服务回归测试中大量使用这个模式,一个复合断言可以覆盖整个业务链路。
4.4 断言工厂模式 —— 统一创建,批量生产
工厂模式解决的是「创建复杂断言对象」的问题。比如你的项目里有多种类型的用户(管理员、普通用户、访客),每种用户的权限断言都不一样。用工厂模式:
class AssertionFactory:
@staticmethod
def create_assertion(user_type):
if user_type == 'admin':
return AssertionComposite() \
.add(lambda r: assert r['role'] == 'admin') \
.add(lambda r: assert r['permissions']['delete'] is True)
elif user_type == 'guest':
return AssertionComposite() \
.add(lambda r: assert r['role'] == 'guest') \
.add(lambda r: assert r['permissions']['read'] is True)
else:
raise ValueError(f"未知用户类型: {user_type}")
# 一行搞定
assertion = AssertionFactory.create_assertion('admin')
assertion.execute(api_response)
你看,调用方完全不需要知道内部怎么组合。工厂模式把「创建」和「使用」解耦了。我建议在断言库的入口层使用工厂,这样团队新成员只需要记住几个工厂方法名,不用关心底层细节。
| 模式 | 核心作用 | 适用场景 | 复用级别 |
|---|---|---|---|
| 函数封装 | 隐藏重复判断细节 | 字段校验、类型检查 | 函数级 |
| 参数化 | 数据与逻辑分离 | 边界值、多组输入 | 数据级 |
| 组合模式 | 动态组装复合断言 | 多条件校验、链路断言 | 对象级 |
| 工厂模式 | 统一创建断言实例 | 多角色、多环境断言 | 架构级 |
最后说一句,这四种模式不是互斥的。我通常会在一个断言库里同时使用它们:用函数封装做原子断言,用参数化处理数据变化,用组合模式构建业务场景,用工厂模式对外暴露统一入口。这样,无论是写新测试还是维护旧用例,都轻松很多。