3 · 断言分类与抽象

说实话,我刚带团队那会儿,大家写的断言五花八门。有人直接在用例里写 assert resp.status_code == 200,有人把数据库字段校验和页面文案混在一起。看着都头疼。后来我意识到——断言必须分类,然后抽象。不然回归测试就是一团乱麻。

3.1 三类断言:业务、技术、通用

我个人习惯把断言分成三个抽屉:业务断言、技术断言、通用断言。你想想看,如果不分,一个函数里既检查 HTTP 状态码,又检查订单金额,还顺带看下时间格式——维护的时候你根本不知道它到底想验证什么。

  • 业务断言:跟需求挂钩。比如“订单总价 = 商品单价 × 数量 + 运费”。这类断言变化最频繁,因为业务逻辑经常改。
  • 技术断言:跟实现细节相关。比如“接口响应时间 < 200ms”、“数据库连接未关闭”。我见过很多团队把技术断言和业务断言写在一起,结果业务一改,技术断言也跟着误报。
  • 通用断言:跟领域无关。比如“对象不为 None”、“列表长度大于 0”、“字符串不为空”。说白了就是最基础的契约检查。
核心观点: 分类不是搞形式主义。分类是为了让断言在合适的层次上被复用,也为了在回归失败时能快速定位——到底是业务逻辑炸了,还是基础设施抽风。

3.2 为什么需要断言抽象层?

我记得有一次,项目里要改支付网关的返回格式。原本所有用例都直接断言 payment['status'] == 'success',结果字段名改成 payment['state'],好家伙,两百多个用例全红了。这就是典型的断言与具体实现耦合。

抽象层,说白了就是在测试代码和被测系统之间加一层“翻译”。你不再直接断言原始响应,而是调用一个 assert_payment_success(payment)。至于内部怎么判断,由抽象层封装。这样支付字段改名,你只需要改一个地方。

我建议每个项目至少维护一个 assertions/ 包,里面按模块分文件。业务断言放一个文件,技术断言放一个,通用断言可以继承或组合。

3.3 抽象层设计:四层结构

下面这张图是我在多个项目中沉淀下来的分层方式。你可以把它当成一个脚手架,不一定照搬,但思路值得参考。

断言抽象层设计 · 四层结构 通用断言层 (GenericAssertions) is_not_none | is_empty | is_greater_than | 类型检查 | 范围校验 技术断言层 (TechAssertions) 响应时间 < 阈值 | 状态码 2xx | DB连接池 | 日志无ERROR | 内存边界 业务断言层 (BizAssertions) 订单金额计算正确 | 用户状态流转 | 支付成功 | 库存扣减一致 场景编排层 (用例层) 组合业务断言 + 技术断言 + 通用断言 → 完整回归场景 抽象程度递减 · 具体程度递增 通用/技术/业务 分层

从图上你能看到:通用断言在最底层,业务断言在中间,最上层是场景编排。每一层只依赖下层,绝不跨层调用。这样做的好处是——当业务逻辑调整时,你只需要动业务断言层,通用和技术断言几乎不受影响。

3.4 代码示例:分层后的断言长什么样

拿一个电商下单场景举例。以前大家可能这么写:

# 反面案例:断言混在一起,难以复用
def test_create_order():
    resp = api.create_order(...)
    assert resp.status_code == 200                     # 技术断言
    assert resp.json()["data"]["total"] == 99.8        # 业务断言
    assert resp.json()["data"]["items"] is not None    # 通用断言

引入抽象层之后,我习惯这样组织:

# 通用断言层 (generic_assertions.py)
def assert_not_none(obj, msg="对象不应为None"):
    assert obj is not None, msg

# 技术断言层 (tech_assertions.py)
def assert_status_ok(resp):
    assert resp.status_code == 200, f"期望200,实际{resp.status_code}"

# 业务断言层 (biz_assertions.py)
def assert_order_total(resp, expected_total):
    data = resp.json()["data"]
    actual = data["total"]
    assert abs(actual - expected_total) < 0.01, f"订单金额不符: {actual} vs {expected_total}"

# 测试用例中只编排,不写原始断言
def test_create_order():
    resp = api.create_order(...)
    assert_status_ok(resp)                 # 技术
    assert_order_total(resp, 99.8)         # 业务
    assert_not_none(resp.json()["data"])   # 通用
我曾经在一个遗留项目里看到有人直接在业务断言里写 time.sleep(2),这其实把等待机制和业务逻辑耦合了。抽象层要纯净——只做校验,不做前置动作或等待。等待应该由专门的等待层或夹具处理。

3.5 分类与抽象带来的实际收益

我统计过,做了断言分类和抽象之后,回归测试的维护成本降低了大概 40%。具体来说:

维度未分类/无抽象分类 + 抽象层
接口字段变更改 20+ 个用例改 1 个业务断言函数
技术栈迁移(如 HTTP2)每个用例都要调只改技术断言层
新人上手成本需要理解每个断言的细节只看业务断言函数名就懂
回归失败定位逐行看断言看哪一层报错即可

说白了,分类和抽象不是炫技,是让你在项目越来越大的时候,还能睡得着觉。

如果你刚开始重构断言,我建议从“业务断言”入手。先把最常变化的逻辑抽成函数,技术断言和通用断言可以慢慢收拢。别想着一步到位,迭代着来。