ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI 写的 Python 代码能跑就够了?用金额计算补上单元测试

AI 写的 Python 代码能跑就够了?用金额计算补上单元测试 AI 写的 Python 代码能跑就够了用金额计算补上单元测试摘要AI 生成的 Python 函数能正常运行却可能在舍入、输入类型和边界条件上偏离需求。通过一个金额计算案例演示如何先定义规则再让 AI 找反例最后用 Decimal 和 unittest 验证修复。附完整代码、测试用例和可复制提示词。输入单价 2.675、数量 1希望保留两位小数程序却返回 2.67。这类问题很适合拿来检查 AI 生成的代码函数很短没有网络请求也没有复杂框架错误却不一定能靠“看起来没问题”发现。例如把“写一个计算总价的 Python 函数保留两位小数”交给 AI可能得到这样的实现defcalculate_total(unit_price,quantity):returnround(float(unit_price)*quantity,2)print(calculate_total(2.675,1))# 2.67代码能够执行结果也确实有两位小数。但如果业务要求这一项输出 2.68它就没有完成任务。这里的代码是教学样例不是对某个模型真实回答的测评。关键在于先把“正确”写成明确约定测试才有判断依据。1. 不要让 AI 替你决定业务规则“保留两位小数”还不足以定义金额计算。至少需要说清输入格式、数值范围、舍入方式以及在哪一步舍入。为了让例子可复现采用下面这份约定项目本例规则单价类型十进制字符串不接受 float单价格式16 位整数可带 13 位小数只接受 ASCII 数字单价范围非负最大 999999.999数量110000 的整数不接受布尔值运算顺序单价乘数量后对总价舍入一次舍入规则ROUND_HALF_UP本例非负数在中点时向上舍入返回值固定两位小数的字符串非法输入类型不符抛 TypeError格式或范围不符抛 ValueError这是教学场景的选择不是所有金额系统的统一标准。真实项目如果要求逐项舍入、不同货币精度或退款负数应该先修改约定再修改实现。例如单价为 0.005、数量为 2先乘再舍入0.005 × 2 0.010结果是 0.01。先把单价舍入为 0.01再乘 2结果是 0.02。两条计算路线都能写成合法代码但不能在需求不清楚时随意替换。图 1预期结果来自规则。旧代码的输出只能作为待检查对象。2. 第一轮提示词先找反例暂时别改代码直接说“帮我优化一下”模型可能优先调整命名、补注释却保留关键行为。更具体的提问方式是请审查下面的 Python 金额计算函数先不要重写。 业务约定 1. 单价只能是非负十进制字符串16 位整数可带 13 位小数。 2. 不接受科学计数法、空格、NaN、Infinity 或 float。 3. 数量只能是 110000 的整数bool 不算合法整数。 4. 先用单价乘数量再按 ROUND_HALF_UP 将总价舍入到两位小数。 5. 返回固定两位小数的字符串。 6. 类型错误抛 TypeError格式或范围错误抛 ValueError。 请输出 - 违反约定的具体行为 - 每个问题对应的最小反例 - 根据约定手工推导的预期结果 - 需要运行验证的地方。 不要把旧函数的输出直接当作测试期望值。 没有运行工具时请明确说“尚未执行”不要声称测试已通过。 待审查代码 def calculate_total(unit_price, quantity): return round(float(unit_price) * quantity, 2)一个有价值的审查应该指出原函数返回 float没有限制数量接受了某些约定之外的输入转换为二进制浮点数后结果可能不符合指定的十进制舍入语义。这些发现都可以落实到测试。反过来“代码不够健壮”没有给出输入和预期就还不能指导修复。3. 修复实现从字符串构造 DecimalPython 的 Decimal 支持十进制运算也能显式指定舍入方式。不要先转成 float再期待 Decimal 恢复原始十进制输入。具体构造和舍入行为可查阅 Python Decimal 官方文档。在一个空目录中创建pricing.py使用 Python 3.10 或更新版本即可运行无需额外安装依赖。importrefromdecimalimportContext,Decimal,ROUND_HALF_UP,localcontextdefcalculate_total(unit_price:str,quantity:int)-str:Multiply first, then round once to two decimal places.ifnotisinstance(unit_price,str):raiseTypeError(unit_price must be a decimal string)ifnotre.fullmatch(r[0-9]{1,6}(?:\.[0-9]{1,3})?,unit_price):raiseValueError(price needs 1-6 integer digits and 0-3 decimal digits)iftype(quantity)isnotint:raiseTypeError(quantity must be an integer, not bool)ifnot1quantity10000:raiseValueError(quantity must be between 1 and 10000)withlocalcontext(Context(prec28,roundingROUND_HALF_UP)):totalDecimal(unit_price)*quantityreturnformat(total.quantize(Decimal(0.01)),.2f)if__name____main__:print(calculate_total(2.675,1))print(calculate_total(0.005,2))实现里有几个决定值得保留入口接受字符串。调用方传入2.675而不是已经转换过的浮点值。遇到 float 直接拒绝让边界清晰。格式检查与数值计算分开。正则表达式约束位数、字符和小数格式顺带排除了 NaN、Infinity、科学计数法、负号和空白。前导零允许存在但仍占整数位数。布尔值单独排除。Python 中 bool 是 int 的子类使用isinstance(True, int)会得到 True。因此这里使用严格类型判断落实“不接受布尔值”的约定。舍入只发生在乘法之后。如果提前对单价调用 quantize就改变了计算规则。使用独立的计算上下文。为这个函数设置精度和舍入规则避免依赖调用方当前的 Decimal 上下文。本例输入上限对应的乘积位数远低于 28 位精度扩大输入范围时应重新检查这一假设。执行python pricing.py输出2.68 0.01这里返回的是用于明确接口约定的字符串。若下一步还要继续运算可以在系统设计中约定返回 Decimal不要为了显示方便又转回 float。4. 测试要覆盖规则而不只是正常路径在同一目录创建test_pricing.pyimportunittestfrompricingimportcalculate_totalclassPricingTests(unittest.TestCase):deftest_normal(self):self.assertEqual(calculate_total(19.90,3),59.70)deftest_half_up(self):self.assertEqual(calculate_total(2.675,1),2.68)self.assertEqual(calculate_total(2.665,1),2.67)deftest_round_after_multiplication(self):self.assertEqual(calculate_total(0.005,2),0.01)deftest_zero(self):self.assertEqual(calculate_total(0,1),0.00)deftest_upper_bound(self):self.assertEqual(calculate_total(999999.999,10000),9999999990.00)deftest_bad_price_format(self):forpricein[-1,NaN,Infinity,1e2, 1,1.0001,1000000,]:withself.subTest(priceprice):withself.assertRaises(ValueError):calculate_total(price,1)deftest_bad_price_type(self):withself.assertRaises(TypeError):calculate_total(2.675,1)deftest_quantity_contract(self):forqtyin[True,1.5,2]:withself.subTest(qtyqty):withself.assertRaises(TypeError):calculate_total(1,qty)forqtyin[0,-1,10001]:withself.subTest(qtyqty):withself.assertRaises(ValueError):calculate_total(1,qty)if__name____main__:unittest.main()在文件所在目录执行python-munittest-v如果本机的 Python 命令是python3替换命令开头即可。unittest 是 Python 标准库中的测试框架命令行用法见 官方说明。这组代码已在本地执行8 个测试方法通过。输出末尾如下运行耗时因环境而异Ran 8 tests in ... OK其中2.665 的案例用来检查指定的中点舍入行为0.005 × 2 的案例检查舍入时机非法输入用例检查函数有没有把约定落实到入口。一个测试方法中可能含有多个子用例因此“8 个测试方法”不等于“仅检查了 8 个输入”。测试结果能说明这些已覆盖行为符合约定不能证明函数在所有系统环境里都没有问题。5. 第二轮提示词让修改范围保持可检查确认反例之后再让 AI 修改代码请根据已经确认的业务约定进行最小修复。 要求 - 保留 calculate_total(unit_price, quantity) 的调用形式。 - 只使用 Python 标准库。 - 使用十进制字符串构造 Decimal。 - 先乘法再对总价按 ROUND_HALF_UP 保留两位小数。 - 为类型、格式和范围错误抛出约定的异常。 - 不增加网络请求、文件读写或第三方依赖。 请交付 1. 修复后的完整函数 2. unittest 测试 3. 每个测试对应的业务规则 4. 实际运行结果无法运行时明确说明 5. 未覆盖的场景。 不要修改预期值来迁就实现。拿到结果后先检查测试里的预期值再执行测试。假如 AI 同时生成实现和测试它可能在两处重复同一个理解错误绿色的测试结果并不能自动纠正需求误解。例如把“先舍入单价”同时写进代码和测试测试会通过计算方式却违反了本例约定。因此关键反例的结果需要依据规则独立推导。6. 接入真实项目还要验证函数之外的边界这份样例只验证单价和数量的计算不包含税费、优惠分摊、退款、汇率和多币种也不决定真实业务应采用哪一种舍入制度。接入接口时检查前端是否已经把单价转成浮点数写入数据库时检查字段精度和转换方式跨服务传递时检查金额是否保持约定的表示形式。函数内部正确不代表整个数据链路没有精度变化。提供上下文给 AI 时优先使用这种最小复现和虚构样例。没有必要为了解释一个舍入问题上传真实订单、客户信息或完整业务仓库。下一次拿到 AI 生成的函数可以先问一句哪一个输入最容易证明它不符合需求找到这个输入再把对应的业务预期写成测试修改结果就有了具体的验收依据。
返回列表