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)

你看,调用方完全不需要知道内部怎么组合。工厂模式把「创建」和「使用」解耦了。我建议在断言库的入口层使用工厂,这样团队新成员只需要记住几个工厂方法名,不用关心底层细节。

模式核心作用适用场景复用级别
函数封装隐藏重复判断细节字段校验、类型检查函数级
参数化数据与逻辑分离边界值、多组输入数据级
组合模式动态组装复合断言多条件校验、链路断言对象级
工厂模式统一创建断言实例多角色、多环境断言架构级
❗ 我曾经踩过的坑: 工厂模式不要滥用。如果你的断言只有两三种,硬编码比工厂更直观。我见过有人为两个断言写了 5 个工厂类…… 过度设计也是债。

最后说一句,这四种模式不是互斥的。我通常会在一个断言库里同时使用它们:用函数封装做原子断言,用参数化处理数据变化,用组合模式构建业务场景,用工厂模式对外暴露统一入口。这样,无论是写新测试还是维护旧用例,都轻松很多。