让代码模型从函数签名补测试用例:怎么压住那些跟业务无关的断言
从函数签名补测试用例,最大的坑不是模型不会写测试,而是它太会写了——会写出大量「看起来对、实际上跟业务一点关系都没有」的断言。压住这类断言的关键,不是让它「少写一点」,而是把业务约束前置到提示词里,让模型从签名之外拿到足够多的判断依据。
先搞清楚:为什么签名补测试必然产生垃圾断言
函数签名能提供的信息,只有参数类型、返回值类型、参数名和返回名。一个 create_user(username: str, age: int) -> User 的签名,模型能推断出的约束极其有限:username 大概不能为空,age 大概不能为负。但真实的业务规则可能是:username 只允许小写字母和数字、长度 3-20、age 必须在 18-65 之间、User 的 id 必须由数据库生成且不可为空。
模型不知道这些,它就只能拿「通用编程常识」来填。于是你会看到:
def test_create_user():
user = create_user("alice", 25)
assert user is not None
assert user.username == "alice"
assert user.age == 25
这些断言在「测试通过」的意义上完全正确,但一条业务规则都没覆盖到。年龄传 17 应该抛异常吗?username 传 "Alice!" 应该拒绝吗?你拿去给团队用,跑一遍全绿,覆盖率高得吓人,实际上全是废的。
所以问题不在「模型写测试」这个动作,而在于它拿到的信息量不足以支撑有业务含义的断言。要压住垃圾断言,就得在提示词里把业务约束补进去——而且要在模型开始生成之前就补进去。
实操:三层提示词结构,把业务约束钉死在生成前
我用过的提示词结构分三层。第一层给角色和任务边界,第二层给业务约束来源,第三层给断言规则。下面拆开讲。
第一层:角色与任务边界,明确「你只能测什么」
这一层的目的是让模型知道,它的测试对象是「业务规则」而不是「代码实现」。实现细节不该出现在测试里,但业务规则必须出现。
你是一个单元测试编写器。你的输入是一个 Python 函数的签名和它的业务约束说明。
你的输出是该函数的 pytest 测试用例。
要求:
- 只测试业务约束说明中明确提到的规则
- 不要测试 Python 语言本身的特性(如类型检查、None 判断)
- 不要测试与业务无关的实现细节(如内部变量赋值、调用次数)
- 每条测试用例的 docstring 必须写明它验证的是哪条业务规则
「不要测试 Python 语言本身的特性」这句话,能砍掉一大半 assert result is not None 和 assert isinstance(result, User) 之类的断言。模型默认会写这些,因为它在训练数据里见得最多。但你明确告诉它不要,它就能压住。
第二层:业务约束来源,让模型有据可依
这一层是整个提示词的核心。业务约束不能靠模型猜,你得给它。来源可以是函数的 docstring、项目的规则文档、或者你手写的约束列表。我的做法是:在签名下面直接跟一段「业务约束」文本,格式固定。
业务约束:
- username: 仅允许小写字母和数字,长度 3-20,不能为空
- age: 整数,范围 18-65(含两端)
- 返回值: User 对象,其 id 字段必须非空且由系统生成
- 异常: username 非法时抛出 ValueError,age 非法时抛出 ValueError
这段文本里,每一条都对应一个「业务规则」。模型拿到这个之后,生成的断言就有了明确指向。实测下来,只要业务约束写得够具体,模型生成的测试用例几乎没有跑偏的。
这里有个细节:约束要写成「可验证的断言」形式,而不是「描述性的语句」。比如写「age 范围 18-65」,模型会生成边界测试;写「age 应该是合理的年龄」,模型就不知道该怎么测了。
第三层:断言规则,直接规定断言怎么写
这一层是最后的兜底。即使有了业务约束,模型有时候还是会写出一些「业务沾边但价值不大」的断言。比如:
def test_create_user_valid():
user = create_user("alice", 25)
assert user.username == "alice"
assert user.age == 25
这个测试测的是「传入什么就返回什么」,业务规则只覆盖了「合法输入能成功创建」这一条,而且断言的方式极其脆弱——如果 create_user 内部对 username 做了规范化处理(比如 trim 空格),这个测试就挂了,但它挂的原因跟业务规则无关。
所以第三层我直接告诉模型断言怎么写:
断言规则:
- 每个测试用例只验证一条业务规则,不要在一个测试里堆多条断言
- 对合法输入,断言返回值的关键字段符合业务约束(如 id 非空),不要断言返回值与输入完全一致
- 对非法输入,断言抛出指定的异常类型和异常信息关键词
- 边界值必须覆盖:最小值、最大值、刚好越界的值
- 不要写 assert True 或 assert False 这类无意义的断言
「不要断言返回值与输入完全一致」这条特别重要。模型默认会写 assert user.username == "alice",因为这是最「自然」的测试。但业务上,create_user 的职责是「创建一个符合规则的用户」,而不是「原样返回输入」。你的断言应该验证业务结果,而不是验证数据搬运。
一个完整的提示词模板
把三层拼起来,就是一个可以直接用的模板。我用这个模板在 GPT-4o 和 Claude 3.5 Sonnet 上都跑过,效果稳定。
你是一个单元测试编写器。你的输入是一个 Python 函数的签名和它的业务约束说明。
你的输出是该函数的 pytest 测试用例。
要求:
- 只测试业务约束说明中明确提到的规则
- 不要测试 Python 语言本身的特性(如类型检查、None 判断)
- 不要测试与业务无关的实现细节(如内部变量赋值、调用次数)
- 每条测试用例的 docstring 必须写明它验证的是哪条业务规则
函数签名:
```python
def create_user(username: str, age: int) -> User:
"""创建用户。
业务约束:
- username: 仅允许小写字母和数字,长度 3-20,不能为空
- age: 整数,范围 18-65(含两端)
- 返回值: User 对象,其 id 字段必须非空且由系统生成
- 异常: username 非法时抛出 ValueError,age 非法时抛出 ValueError
"""
```
断言规则:
- 每个测试用例只验证一条业务规则,不要在一个测试里堆多条断言
- 对合法输入,断言返回值的关键字段符合业务约束(如 id 非空),不要断言返回值与输入完全一致
- 对非法输入,断言抛出指定的异常类型和异常信息关键词
- 边界值必须覆盖:最小值、最大值、刚好越界的值
- 不要写 assert True 或 assert False 这类无意义的断言
请生成完整的 pytest 测试用例。
这个模板跑出来的结果,基本没有 assert result is not None 这种废话,也没有「传入什么就断言返回什么」的搬运式断言。边界值测试会覆盖 17、18、65、66 这种关键节点。
进阶:用「规则编号」强制模型可追溯
如果函数比较复杂,业务约束有十几条,模型生成的测试可能会漏掉几条。我的解决办法是给每条业务约束编号,然后在提示词里要求模型在 docstring 里引用编号。
业务约束:
- [BR-01] username: 仅允许小写字母和数字,长度 3-20,不能为空
- [BR-02] age: 整数,范围 18-65(含两端)
- [BR-03] 返回值: User 对象,其 id 字段必须非空且由系统生成
- [BR-04] 异常: username 非法时抛出 ValueError,age 非法时抛出 ValueError
要求:每条测试用例的 docstring 必须以 [BR-xx] 开头,标明它验证的业务规则编号。
这样生成的测试长这样:
def test_create_user_username_too_short():
"""[BR-01] username 长度小于 3 时抛出 ValueError。"""
with pytest.raises(ValueError, match="username"):
create_user("ab", 25)
好处有两个:一是你能一眼看出哪条规则没被覆盖;二是模型在生成时有了「必须引用编号」的约束,会更仔细地逐条过业务约束,漏掉的概率明显降低。
什么时候这套方法会失效
这套方法的前提是:业务约束能写清楚。如果函数签名本身就模糊,或者业务规则连你自己都说不清,那模型生成出来的测试必然也是一团糟。比如一个 process(data: dict) -> dict 的签名,没有任何业务约束,模型只能瞎猜。
另一个失效场景是:函数有大量副作用,或者依赖复杂的外部状态。签名里看不到这些,业务约束里也很难写全。这种情况我会改用另一种策略——先让模型分析函数体,生成一份「业务规则清单」,人工确认后再让它写测试。这个流程复杂一些,但能覆盖签名信息量严重不足的情况。
常见问题
Q: 为什么我让模型写测试,它总是写 assert result is not None 这种废话?
A: 因为这是模型训练数据里最常见的测试模式。签名里没有业务约束,模型就只能用「不为空」「类型正确」这类通用断言来填。解决办法是把业务约束写进提示词,并且明确告诉它「不要测试 Python 语言本身的特性」。
Q: 业务约束要写到多细才够?
A: 写到「可以逐条翻译成断言」的程度。比如「age 必须是合理年龄」就不够细,模型不知道合理是 0-120 还是 18-65。写成「age 范围 18-65(含两端)」就够细,模型能直接生成边界测试。
Q: 函数签名里的参数名和返回类型信息,模型会用到吗?
A: 会用到,但只用来做「类型层面的推断」。比如 age: int 会让模型知道 age 是整数,但不会告诉它范围是 18-65。签名信息只能支撑最基本的断言,业务约束才是核心。
Q: 如果业务约束很多(超过 20 条),一个提示词塞得下吗?
A: 塞得下,但模型可能会漏掉几条。建议用编号方式([BR-01]、[BR-02])标注每条约束,并要求模型在 docstring 里引用编号。这样漏了哪条一目了然,也方便后续补测。