用测试和决策记录支持代码演进
以整数折扣规则说明行为契约、舍入边界与测试层级,用小步重构和决策记录保留代码演进的依据。
保护费用计算的边界行为,提取变化点,并用 ADR 区分结构重构与规则修改。
费用计算函数出现了越来越多的条件,修改一个折扣规则会影响其他分支。重构之前先定义对外行为:价格单位、折扣表达、舍入方法与异常。测试保护这些行为,决策记录解释结构为什么这样安排,两者配合才能让后来的修改有依据。
案例以整数最小货币单位表示教材价格,用 basis points 表示折扣。一万个基点代表全部折扣,1500 代表百分之十五。数值是教学设定,不涵盖真实支付平台、税费或地区币种规则。
行为契约先于代码结构
重构改变内部组织,并保留约定的可观察行为。输入校验、返回值、异常和副作用都可能属于行为,不能只比较正常结果。若把无效价格从抛异常改成默默返回零,就已经改变契约,需要作为功能变更单独说明。
先调查调用方怎样使用函数:是否允许零价格、折扣能否为一百、返回单位是否一致、错误是否用于提示。发现原来的行为不合理时,可提出新规则,但应区分保留行为的重构与修正行为的变更,避免在一次结构调整里悄悄改变计费。
测试应表达业务例子与边界,而不是逐行重复算法。一个已知价格与折扣的结果能保护核心规则;零、最大折扣、无折扣和错误类型保护边界。仅断言某个私有函数被调用几次,可能在合理重构后失败,却没有保护用户结果。
用精确单位控制计算含义
浮点数适合许多数值任务,但某些十进制金额不能被二进制浮点精确表示。使用最小单位整数可以避免这类表示误差,仍必须定义舍入。整数除法会丢掉余数,本例明确向下取整最终金额,不能把它写成所有商业场景的标准规则。
下面拒绝负价格、不合法折扣与非整数输入。type(x) is int 也排除了 Python 中作为 int 子类的 bool,避免 True 被当成一单位价格。
def discounted(price, basis_points):
if type(price) is not int or price < 0:
raise ValueError("invalid price")
if type(basis_points) is not int or not 0 <= basis_points <= 10000:
raise ValueError("invalid rate")
return price * (10000-basis_points)//10000
assert discounted(1000, 1500) == 850
assert discounted(0, 1500) == 0
assert discounted(999, 0) == 999
try:
discounted(-1, 1000)
raise AssertionError("invalid input accepted")
except ValueError:
pass
print(discounted(1000, 1500))
实际输出是 850,表示原价 1000 最小单位折扣百分之十五后的金额。负价格触发 ValueError;正常零价格和无折扣也通过检查。示例仅运行标准库 Python,不使用 pytest 或 unittest 框架,也没有接真实计费系统。
还应加入有余数的例子,例如 999 与 1500 基点的结果为 849,表明这里向下取整。真实产品可能要求四舍五入、银行家舍入或分行计算后再汇总,应以业务与适用规则为准。更换规则时先写对照例子,再修改实现。
不同测试层覆盖不同边界
单元测试隔离纯规则,运行快且容易指出错误输入。集成测试核对数据库、外部接口与序列化契约。端到端检查从用户入口走到可观察结果,范围更广,也更受环境影响。根据风险选层级,不需要让每条规则都重复走完整浏览器流程。
mock 能控制依赖返回,适合制造超时与拒绝,但也可能隐藏接口不匹配。如果 mock 使用了真实服务从未返回的字段,测试通过仍不能说明集成正确。保留少量针对真实边界的契约验证,核对输入、输出、状态码和失败条件。
pytest 支持直接 assert 与 fixture,unittest 提供测试组织、准备和清理等标准库机制。框架帮助管理测试,不会自动选择正确用例。测试数据要可复现,失败记录应说明输入与预期,避免把不稳定的时间或随机输出当成无条件断言。
结构演进从变化点出发
如果只有一个折扣规则,简单函数可能足够。出现不同地区或套餐策略时,可以把稳定的输入检查与可变计算分开,通过组合或 strategy 方式注入规则。设计模式提供讨论变化与耦合的词汇,并不要求每个值都对应一个类。
逐步替换结构:先增加契约测试,再提取一个变化点,运行相关检查,最后更新调用方。小步变更容易定位失败,也便于撤回。重复代码可能有不同业务含义,合并前应确认它们是否应该共同变化,而不是只比较文本相似度。
代码注释适合解释不明显的原因,例如为什么此处向下取整。用注释重复赋值语句通常没有帮助。技术文档需要说明前置条件、单位、输入输出、异常、使用版本与恢复方式,让调用者能判断是否适合自己的路径。
决策记录保留当时的约束
ADR 可以记录上下文、候选方案、决定、状态和后果。案例决定用整数最小单位,原因是输入与结算规则已经采用整数;代价是必须明确舍入并限制单位转换。另一方案是 Decimal,可支持更直接的十进制计算,但仍要管理精度和舍入上下文。
把重新讨论的条件写进去:如果支持具有不同最小单位的币种,或需要税费分项,就重新核查这个规则。后来改变决定时,新增记录并标明旧决定被替代,便于理解历史。只写“选择整数因为更好”无法解释取舍。
开源库也属于技术选型。评估许可证、维护状态、兼容版本、行为范围与替换成本,避免仅凭热度引入依赖。测试通过是局部证据,仍需要确认许可证和运营条件;不应虚构某个库已经在本案例生产使用。
失败用例也需要检查具体异常类型。若测试捕获所有异常,语法错误或字段缺失也可能被误认成正确拒绝。为调用边界注明预期异常与错误信息范围,避免依赖不稳定的完整错误栈。清理测试资源时核对所有分支,防止上一个用例的状态污染下一个结果。
练习:拆开重构与规则修改
为折扣函数补齐边界测试,包括零、一单位、带余数、大金额、零折扣、全部折扣、负值、浮点与布尔输入。写一份行为说明,再把计算策略提取出来,要求所有既有行为保持一致。
验收时,相同有效输入的返回单位与舍入不变,无效输入仍按约定拒绝;新增结构不会让调用方绕过校验。然后提出另一种舍入规则,单独写出受影响例子与决策记录,不能把它混称为重构。保留变更前后检查结果与可恢复提交,若涉及数据迁移,再增加独立的兼容与恢复验证。
资料来源与延伸阅读
资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。