给 AI 生成的 Python 代码加一道最小验证环:用 unittest 固定输入和输出

举报
yd_215017870 发表于 2026/11/01 12:26:42 2026/11/01
【摘要】 用整数分和 unittest 为 AI 生成的 Python 代码建立可复现的验收基线:覆盖正常计算、折扣舍入、空输入、输入不变性与非法边界。本文给出完整示例、运行命令和验证边界,说明测试通过能证明什么,以及不能替代哪些检查。

AI 可以很快写出一个函数,但“看起来合理”不是可交付标准。真正省时间的做法,是在接收代码时同时建立一条短而确定的验证环:先写清输入和输出,再用 unittest 把正常路径、边界和异常固定下来,最后重复运行确认结果一致。

本文不讨论云端服务,也不把一次本机测试包装成平台验证。示例只使用 Python 标准库,在本机计算一个购物车报价,金额全部用整数分表示。目标不是展示复杂业务,而是提供一份可以交给 AI、也可以交给人工审查的最小验收模板。

一、先把“正确”写成可比较的结果

假设需求是:给定若干商品的单价和数量,计算小计;再按整数百分比计算折扣;折扣金额向下取整到分;返回小计、折扣和应付总额。

这段需求有几个容易被生成代码忽略的决定:

  • 单价用整数分,不用二进制浮点表示人民币金额。
  • 数量必须是正整数,单价必须是非负整数。
  • 折扣必须是 0 到 100 的整数,True 也不能偷偷当成 1 接受。
  • 空商品列表是合法输入,三个结果都应为 0。
  • 计算函数不能通过排序、删除或回写字段改变调用方的输入。
  • 例如两件 1299 分的笔记本和一支 250 分的笔,小计应精确为 2848 分;999 分打 15 折时,折扣是 999 * 15 // 100 = 149 分,应付 850 分。

这些不是测试框架替我们决定的业务规则,而是测试前必须先写下的验收契约。没有明确的期望值,测试很可能只是重复执行代码本身,无法发现代码与需求的差异。

二、最小循环:生成、固定、运行、解释

一个实用的最小循环可以压缩成四步:

  1. 生成:让 AI 先给出函数和测试所需的输入输出表,不要只要实现代码。
  2. 固定:把关键例子写成 unittest 断言,把边界和错误输入单独列出。
  3. 运行:用固定命令执行同一个文件,记录 Python 版本、测试数量和退出码。
  4. 解释:测试通过只说明这些断言在当前环境成立;仍要检查需求、依赖、性能、权限和外部系统行为是否超出了测试范围。

这里的“固定”很重要。随机数、当前时间、网络响应和未排序的集合都会让失败难以重现。最小验证环先用字面量输入和精确字典输出建立稳定基线,再按需要增加参数化测试、模拟对象或属性测试。

三、一个可运行的完整例子

下面的文件保存为 ai-python-unittest-loop-demo.py。实现和测试使用同一个文件,便于复制到干净目录后直接运行;真实项目可以再把实现和测试拆成不同模块。示例不安装第三方包,也不读写文件、数据库或网络。

"""A small deterministic example for verifying generated Python code."""
from __future__ import annotations

import unittest
from typing import Iterable, Mapping


Item = Mapping[str, object]


def quote_total(items: Iterable[Item], discount_percent: int = 0) -> dict[str, int]:
    """Return a deterministic integer-cent total for a sequence of items."""
    if type(discount_percent) is not int or not 0 <= discount_percent <= 100:
        raise ValueError("discount_percent must be an integer from 0 to 100")

    subtotal_cents = 0
    for item in items:
        unit_cents = item.get("unit_cents")
        quantity = item.get("quantity")
        if type(unit_cents) is not int or unit_cents < 0:
            raise ValueError("unit_cents must be a non-negative integer")
        if type(quantity) is not int or quantity <= 0:
            raise ValueError("quantity must be a positive integer")
        subtotal_cents += unit_cents * quantity

    discount_cents = subtotal_cents * discount_percent // 100
    return {
        "subtotal_cents": subtotal_cents,
        "discount_cents": discount_cents,
        "total_cents": subtotal_cents - discount_cents,
    }


class QuoteTotalTests(unittest.TestCase):
    def test_basic_total_is_deterministic(self) -> None:
        items = [
            {"name": "notebook", "unit_cents": 1299, "quantity": 2},
            {"name": "pen", "unit_cents": 250, "quantity": 1},
        ]
        expected = {
            "subtotal_cents": 2848,
            "discount_cents": 0,
            "total_cents": 2848,
        }
        self.assertEqual(quote_total(items), expected)

    def test_discount_rounds_down_to_a_cent(self) -> None:
        self.assertEqual(
            quote_total([{"unit_cents": 999, "quantity": 1}], 15),
            {
                "subtotal_cents": 999,
                "discount_cents": 149,
                "total_cents": 850,
            },
        )

    def test_empty_input_has_zero_totals(self) -> None:
        self.assertEqual(
            quote_total([]),
            {"subtotal_cents": 0, "discount_cents": 0, "total_cents": 0},
        )

    def test_input_is_not_mutated(self) -> None:
        items = [{"name": "book", "unit_cents": 1000, "quantity": 2}]
        before = [item.copy() for item in items]
        quote_total(items, 10)
        self.assertEqual(items, before)

    def test_invalid_discount_is_rejected(self) -> None:
        for value in (-1, 101, True, 10.5):
            with self.subTest(value=value):
                with self.assertRaisesRegex(
                    ValueError, "discount_percent must be an integer"
                ):
                    quote_total([], value)  # type: ignore[arg-type]

    def test_invalid_item_boundaries_are_rejected(self) -> None:
        cases = [
            ({"unit_cents": -1, "quantity": 1}, "unit_cents"),
            ({"unit_cents": 100, "quantity": 0}, "quantity"),
            ({"unit_cents": 100, "quantity": True}, "quantity"),
            ({"unit_cents": 10.5, "quantity": 1}, "unit_cents"),
        ]
        for item, message in cases:
            with self.subTest(item=item):
                with self.assertRaisesRegex(ValueError, message):
                    quote_total([item])


if __name__ == "__main__":
    unittest.main(verbosity=2)

运行命令:

python ai-python-unittest-loop-demo.py

unittest 的每个方法名都在说明一个可复核的行为。第一个测试锁定普通计算;第二个锁定舍入规则;第三个锁定空输入;第四个锁定不修改输入;最后两组测试锁定非法折扣、负单价、零数量、布尔数量和浮点单价等边界。assertRaisesRegex 同时检查异常类型和错误信息的一部分,避免实现用完全不同的错误掩盖输入契约变化。

四、如何读一次通过结果

本机运行这份示例时,使用的是 Python 3.13.14。输出中的关键事实是:共运行 6 个测试,耗时 0.001 秒,结果为 OK,进程退出码为 0。六个测试方法各出现一次 ok,没有依赖网络、当前时间或随机种子。

这能支持几个有限结论:在该次本机运行环境中,当前实现满足这 6 条断言;同一组固定输入产生了预期字典;列出的非法边界被拒绝。它不能支持“代码已经适合所有订单系统”这样的更大结论,也不能推导出云平台性能、并发安全、数据库一致性或真实用户输入覆盖率。

如果 AI 只返回“测试通过”而没有命令、版本、测试数量和退出码,可以要求它补齐验证记录。若要把结果放进持续集成,至少保留失败时的完整 traceback,并让非零退出码阻止后续交付步骤。

五、测试通过后仍要检查什么

最小验证环是入口,不是替代评审。把生成代码接入真实项目之前,还应按风险补充检查:

  • 需求边界:税费、优惠券、退款、负数金额、超大整数和精度规则是否另有约定。
  • 数据形状:实际输入是否可能缺字段、包含额外字段、来自 JSON,或使用 None。
  • 副作用:函数是否应当写库、发消息或调用接口;这些副作用要用隔离的 fake 或集成环境测试。
  • 安全性:是否把用户字符串拼进 SQL、命令、模板或路径;这不是几条金额断言能覆盖的。
  • 可维护性:异常类型和错误信息是否符合项目约定,类型注解是否与运行时数据一致。
  • 性能和并发:大批量数据、重复调用、超时、重试和并发交错需要专门的测试方法。

测试输入应该来自需求和风险,而不是为了让生成代码“看起来覆盖率很高”随意堆数字。每增加一个规则,就增加一个能失败的断言;每个断言都应能说明失败后应该改需求、改测试还是改实现。

六、明确边界:这条环验证了什么

这份例子验证的是一个无外部依赖、确定性较强的纯计算函数。它没有验证:

  • Python 之外的运行时、操作系统差异或第三方库兼容性;
  • 网络服务、云资源、身份权限、数据库事务或消息队列;
  • 多线程、多进程、异步任务的时序与竞态;
  • 生产数据规模下的内存、延迟和吞吐;
  • 需求本身是否完整、金额规则是否符合法务或财务要求。

因此文章中的环境和输出只代表本机这一次运行记录,不是任何华为云服务的测量,也不是平台验证。边界写清楚,测试结果才不会被误读为超出证据范围的承诺。

总结

面对 AI 生成的 Python 代码,可以先要求一个小而完整的闭环:确定输入,写出精确输出,覆盖正常路径和边界,用 unittest 运行并保留版本、测试数量和退出码,然后明确没有验证的部分。整数金额、固定字面量和不依赖外部状态的断言让结果容易复现;错误输入和输入不变性测试则能快速暴露“能运行但不符合契约”的实现。

这不是把测试数量当质量分数,而是先建立一条可观察、可重复、可解释的最低反馈路径。代码复杂度和风险上升时,再沿着同一条路径扩展到模块测试、集成测试和并发实验。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。