陷阱三:断言过于宽泛——测试通过了,但真的测对了吗?

说实话,这个坑我踩过不止一次。

有一次,我负责一个支付系统的接口测试。写了个断言,大概长这样:assert response.status_code == 200。测试跑通了,绿油油一片,心里美滋滋。结果上线当天,用户反馈说「支付成功了,但余额没扣」。

你猜怎么着?接口确实返回了200,但返回的JSON里有个 "success": false。我的断言根本没检查这个字段。

嗯,这就是典型的「断言过于宽泛」。测试通过了,但真的测对了吗?

什么是「宽泛断言」?

说白了,就是你的断言只覆盖了结果的一小部分。就像你检查一个人有没有出门,只看他穿了鞋,却没看他是不是还在床上躺着。

常见的宽泛断言有这些:

  • 只检查HTTP状态码(200就完事了)
  • 只检查返回不为空(有数据就行,不管对不对)
  • 只检查某个字段存在(不检查值是否正确)
  • assert True 这种毫无意义的断言

我个人习惯,把断言分成三个层次:

层次 检查内容 示例
第一层 状态码/协议层 assert response.status_code == 200
第二层 业务状态码/错误码 assert data["code"] == 0
第三层 核心业务数据 assert data["balance"] == expected_balance

很多测试只做到第一层,甚至第二层。第三层才是真正「测对了」的关键。

一个真实的例子

我在项目中遇到过这样一个场景:测试一个用户注册接口。

宽泛的写法是这样的:

def test_register():
    response = client.post("/register", json={"username": "test", "password": "123456"})
    assert response.status_code == 200
    # 完了,就这一句

这个测试能发现什么问题?几乎什么都发现不了。就算后端返回了 {"error": "用户名已存在"},只要状态码是200,测试照样绿。

那应该怎么写?我建议这样:

def test_register_success():
    response = client.post("/register", json={"username": "test_user_001", "password": "123456"})
    assert response.status_code == 200
    
    data = response.json()
    assert data["code"] == 0  # 业务成功码
    assert data["data"]["username"] == "test_user_001"
    assert data["data"]["user_id"] is not None
    assert len(data["data"]["token"]) > 0  # 确保生成了token

你看,这才叫「测对了」。每个关键字段都验证了,任何一个环节出问题,测试都会红。

为什么我们会写出宽泛断言?

原因其实挺简单的:

  • ——写详细的断言确实要多花几分钟
  • ——项目排期紧,先跑通再说
  • 不懂业务——不知道哪些字段是关键字段
  • 过度信任代码——觉得「应该不会出问题吧」

但你想过没有?一个宽泛的断言,等于没断言。它给你一种「测试通过了」的错觉,实际上漏洞百出。

如何写出精准的断言?

我总结了几个原则,你可以参考:

  1. 每个关键字段都要断言——不是所有字段,但业务核心字段必须覆盖
  2. 断言值的正确性,而非存在性——别只检查字段有没有,要检查值对不对
  3. 考虑边界和异常——正常情况测了,异常情况呢?空值、超长值、特殊字符呢?
  4. 使用数据驱动——同一个接口,多组数据,多组断言

举个例子,测试一个查询商品列表的接口:

def test_product_list():
    response = client.get("/products?page=1&size=10")
    assert response.status_code == 200
    
    data = response.json()
    assert data["code"] == 0
    assert len(data["data"]["items"]) <= 10  # 分页大小正确
    assert data["data"]["total"] >= 0  # 总数非负
    # 检查第一个商品的关键字段
    if data["data"]["items"]:
        item = data["data"]["items"][0]
        assert "id" in item
        assert "name" in item
        assert "price" in item
        assert isinstance(item["price"], (int, float))
        assert item["price"] > 0  # 价格必须大于0

核心原则:断言要精确到「如果业务逻辑错了,测试一定能发现」的程度。而不是「只要没报错就算过」。

避坑指南

我曾经接手过一个项目,里面有2000多个测试用例,跑起来全绿。结果一上线,核心功能直接崩了。

为什么?因为大部分断言都是 assert response.status_code == 200。接口返回了200,但返回的是个空壳数据。

那次之后,我定了个规矩:每个接口测试,至少要有3个以上的具体断言。少了就驳回。

一个小技巧:写断言的时候,可以问自己一个问题——「如果这个断言删掉,测试还能发现这个bug吗?」

如果能,说明这个断言不够。如果不能,说明这个断言是必要的。

用一张图总结

下面这张图,展示了断言从宽泛到精准的演进路径:

断言精准度演进路径 阶段一:宽泛断言 只检查状态码 assert status == 200 阶段二:基础断言 检查业务状态码 assert data["code"] == 0 阶段三:精准断言 验证核心业务数据 assert balance == 100 三种断言对比 对比维度 宽泛断言 基础断言 精准断言 发现bug能力 极低 中等 误报率 低(但漏报高) 中等 维护成本 中等 稍高(但值得)

从这张图可以看得很清楚:宽泛断言虽然维护成本低,但发现bug的能力也低。精准断言虽然要多写几行代码,但它是真正能拦住问题的防线。

最后说两句

写测试不是为了「跑绿」,而是为了「发现问题」。一个永远绿的测试,如果什么都发现不了,那它就是个摆设。

我个人习惯,每次写完测试,都会故意改错一行代码,看看测试能不能发现。如果发现不了,说明断言还不够精准。

嗯,这个习惯帮我拦下了不少线上事故。你也可以试试。


公众号:蓝海资料掘金营,微信deep3321