Python后端工程化与测试13:代码能跑,还需要Ruff和类型检查吗

发布时间:2026/10/24 6:33:26
Python后端工程化与测试13:代码能跑,还需要Ruff和类型检查吗 Python后端工程化与测试13代码能跑还需要Ruff和类型检查吗一次重构后pytest全部通过但同事发现两个问题新函数声明返回整数某条分支却返回了字符串另一个文件仅调整了格式代码审查里却出现几十行无意义差异。前者可能是测试未覆盖的类型错误后者让真正的业务变化更难被看见。它们不适合靠增加十个类似的接口测试解决也不应该全推给审查者的眼力。这一课为QuoteLedger建立三道各司其职的检查Ruff处理静态错误与统一格式mypy检查应用的类型合同pytest继续验证运行行为。它们最终进入同一个命令入口下一篇再把这个入口放进持续集成。我们会故意制造坏代码证明这些检查能够拒绝错误避免把“装了几个工具”当成工程化完成。E12答案随机生成之前先保留能手算的例子上一课要求金额、不可变对象和状态机三组性质。完整答案位于lessons/13/test_answer.py在课程根目录、安装好最终项目后执行python-m pytest lessons/13/test_answer.py-q python lessons/13/mutation_answer.py第一个命令运行两项性质测试每项max_examples30并关闭单例执行时间上限deadlineNone。关闭deadline是为了不把机器忙碌误判为业务失败不是给测试整个进程无限运行时间全套验收仍会限制命令总时间。第一项生成合法数量和金额直接断言单价乘数量再加一次运费。显式example(quantity2, unit_cents5, shipping_cents3)保证13分这条重要回归始终出现不依赖随机生成碰巧选到。第二项先保存原Quote的字段快照再执行合法确认或取消检查旧对象保持原样新对象身份、金额和创建时间不变只有目标状态和递增一次的version变化。最后对两个终态再次尝试两种转换都必须抛InvalidState。这里传入当前version故意排除版本冲突的干扰我们正在验证终态规则而不是把两个错误混在一个断言中。性质测试没有证明数据库并发原子性。它只调用领域公开函数不创建真实SQLite文件同样没有证明HTTP入口拒绝布尔值或多余字段。因此前两篇的API、真实数据库与竞态测试仍要保留。测试类型之间应补充证据不能因为生成了几十个例子就替换其他层的测试。第二个命令只在临时目录复制domain.py把运费公式故意改成随数量重复收费再运行金额测试。预期这个子测试退出1并出现AssertionError外围答案脚本确认它确实失败才报告实验通过。reports/mutation-e12.json保存失败摘要原始源码前后字节一致。如果变异后测试仍然全绿说明断言或生成空间存在缺口不能简单宣布“变异实验完成”。Ruff不是另一套业务测试Ruff可以发现未定义名称、未使用导入、部分明显错误并统一导入和格式。比如把quote_id误拼成未定义的qoute_id这种错误不必等某个HTTP请求刚好走到那条分支才暴露。它不会知道“运费应该只收一次”因为错误公式仍然是合法Python业务测试才理解这个承诺。格式检查的价值也不只是看起来整齐。所有人使用同一格式后一个PR里出现的差异更接近真实逻辑变化审查者不用反复讨论空格和换行。不要在修复状态机的一次提交中顺手格式化全仓库造成几百行噪音。可以先单独提交纯格式变更再提交逻辑改动每一步都运行测试。在project目录运行python-m ruff check src tests python-m ruff format--check src testscheck运行lint规则format --check只判断当前文件是否符合格式不修改文件。开发中确认需要修正时可以运行ruff format src tests再查看变化。check --fix也只能自动解决一部分问题不能把它理解成“一键修复程序”。规则来自pyproject范围是应用与测试早期历史阶段保留当课形态不要求为了最终格式统一而篡改学习过程。mypy为什么能发现测试没执行到的问题假设函数签名是def total(value: int) - int实现却return wrong。mypy无需执行函数就能发现返回值违反类型声明pytest只有在某条测试调用并检查相应返回值时才可能发现。静态分析与动态测试的工作方式不同所以组合使用才有意义。应用源码启用严格检查是为了让模块边界可信仓库返回Quote而不是任意字典service获取的是明确的clock与IDFactory状态值有有限范围。不要为了快速变绿在配置里加入ignore_errorstrue那相当于把门禁的报警器拔掉。遇到类型报错先读数据从哪里进入、怎样缩小范围确实需要类型断言时也应该解释该断言依据什么运行约束。类型检查不会执行Pydantic不会证明来自网络的JSON真的满足注解。quantity: int只是开发者承诺HTTP请求仍需要严格输入模型领域构造仍需要业务校验。尤其bool是int的子类外部输入的“严格整数”合同不能只依靠静态提示。你在类型工具里看到全绿只代表它在给定分析范围内未发现矛盾不是安全审计或完整正确性证明。python-m mypy src本课程严格检查应用src测试主要由Ruff和实际执行保护。这个范围是有意选择测试中的动态夹具、参数化和故障注入与应用公共合同承担的责任不同。如果团队希望测试也严格typing可以逐步扩大范围不要在没有实践需求时为了追求“所有工具全部最严格”让教程变成注解体操。一条门禁命令如何保持可解释从课程根目录运行python tools/quality.py脚本按顺序运行lint、format检查、mypy最后pytest分支覆盖。每步使用sys.executable -m确保调用当前虚拟环境的模块而不是PATH里另一个Python装的工具工作目录明确为project避免读错配置。任何一步非零退出脚本立刻同样失败后面的绿色结果不能覆盖前面的失败。测试阶段使用--covquoteledger --cov-branch覆盖应用包并统计分支。当前85%门槛针对启用分支统计后的综合覆盖率已覆盖语句与分支合计除以可执行语句与分支合计并非仅分支比例。本轮综合91.09%其中语句93.72%、分支77.50%具体原始数字见验收说明。这个门槛帮助发现覆盖明显退化不是“正确率85%”。看到缺失行应先理解风险事务回滚、状态冲突和异常关闭值得优先补为了提高百分比专门断言日志字符串是否存在却不检查失败后的数据状态可能仍然放过关键错误。覆盖报告生成reports/coverage.json供本次验收记录。应用各模块的覆盖数字以实际执行为准不要用一个总百分比掩盖迁移或仓库完全没测。变异实验则补上另一条证据至少针对运费规则错误实现确实会被现有测试发现。它只是一个小型、透明的变异演示没有声称实现完整mutation testing平台。工具报错时怎样处理格式错误先确认是否仅空白变化运行formatter后重新看diff。lint出现F821先找名称来源不要在错误行加忽略。mypy报告return-value时沿函数全部分支检查返回类型尤其异常后有没有返回None不要用cast把一个真正的字符串宣称成整数。pytest失败则回到业务合同判断是实现错误、测试预期错误还是有意改变了外部行为。有意新增version合同属于功能变更应更新调用方、API模型和对应测试。把数据库访问提取成仓库属于重构正常业务响应应该保持。两者若混在一个大提交中很难判断失败来自哪一部分。本课程保留阶段目录就是为了让这种差异可查看日常工作则可以借助小提交和代码审查保持同样的清晰度。练习E13让门禁先红一次在临时目录新建小模块声明接收int并返回int却返回字符串。运行mypy strict并断言命令非零退出且报告return-value修成return value 3后必须通过。接着故意破坏该模块的格式ruff format --check必须失败格式化后再次检查必须通过最后调用函数确认total(10)13。不要改坏正式project不要只打印“已模拟失败”。必须检查子进程退出码保留诊断输出并说明格式修复为什么没有改变业务。下一篇给出完整答案与运行结果。这样你会亲眼看到每一道门禁拦住的究竟是哪一种问题。本课完整代码下面列出E12性质、隔离变异和统一质量入口的完整实现另列项目质量配置。配置文件中的参数与上文命令共同定义检查范围不能把配置当装饰。lessons/13/test_answer.pyE12答案金额、不可变性、版本与终态性质各30个生成例子。fromdataclassesimportasdictfromdatetimeimportdatetime,timezonefromhypothesisimportexample,given,settings,strategiesasstimportpytestfromquoteledger.domainimportCreateQuote,InvalidState,make_quote,transitionsettings(max_examples30,deadlineNone)example(quantity2,unit_cents5,shipping_cents3)given(quantityst.integers(1,100),unit_centsst.integers(0,100000000),shipping_centsst.integers(0,100000000))deftest_amount_charges_shipping_once(quantity,unit_cents,shipping_cents):quotemake_quote(CreateQuote(KB-001,atlas,quantity,unit_cents,shipping_cents),quote_idfixed-id,nowdatetime(2026,1,1,tzinfotimezone.utc))assertquote.total_centsunit_cents*quantityshipping_centsassertquote.total_cents-unit_cents*quantityshipping_centssettings(max_examples30,deadlineNone)given(quantityst.integers(1,100),unit_centsst.integers(0,100000000),shipping_centsst.integers(0,100000000),targetst.sampled_from([confirmed,cancelled]))deftest_transition_keeps_original_and_rejects_terminal(quantity,unit_cents,shipping_cents,target):beforemake_quote(CreateQuote(KB-001,atlas,quantity,unit_cents,shipping_cents),quote_idfixed-id,nowdatetime(2026,1,1,tzinfotimezone.utc))snapshotasdict(before)changedtransition(before,target,expected_version1)assertasdict(before)snapshotassertchangedisnotbeforeassertchanged.idbefore.idandchanged.created_atbefore.created_atassertchanged.total_centsbefore.total_centsassertchanged.statustargetandchanged.version2fornext_targetin[confirmed,cancelled]:withpytest.raises(InvalidState):transition(changed,next_target,expected_version2)lessons/13/mutation_answer.pyE12变异答案临时复制领域模块证明错误的运费公式会被测试抓住。importjsonimportosfrompathlibimportPathimportshutilimportsubprocessimportsysimporttempfile ROOTPath(__file__).resolve().parents[2]defmain():originalROOT/project/src/quoteledger/domain.pyoriginal_bytesoriginal.read_bytes()sourceoriginal.read_text(encodingutf-8)oldself.unit_cents * self.quantity self.shipping_centsnewself.unit_cents * self.quantity self.shipping_cents * self.quantityassertsource.count(old)1,领域公式发生变化需要先审阅变异位置不能猜测替换withtempfile.TemporaryDirectory(prefixquoteledger-mutation-)asfolder:sandboxPath(folder)packagesandbox/quoteledger;package.mkdir()(package/__init__.py).write_text(,encodingutf-8)(package/domain.py).write_text(source.replace(old,new),encodingutf-8)shutil.copyfile(Path(__file__).with_name(test_answer.py),sandbox/test_mutant.py)envos.environ.copy();env[PYTHONPATH]str(sandbox);env[PYTHONUTF8]1resultsubprocess.run([sys.executable,-m,pytest,-q,--rootdir,str(sandbox),test_mutant.py::test_amount_charges_shipping_once],cwdsandbox,envenv,capture_outputTrue,textTrue,encodingutf-8,timeout30)assertresult.returncode1,result.stdoutresult.stderrassertAssertionErrorinresult.stdoutandexplicit exampleinresult.stdout.lower(),result.stdoutassertoriginal.read_bytes()original_bytes reportdict(statusPASS,expected_test_exit1,source_unchangedTrue,outputresult.stdoutresult.stderr)(ROOT/reports/mutation-e12.json).write_text(json.dumps(report,ensure_asciiFalse,indent2),encodingutf-8)print(PASS: mutant killed by explicit amount regression; original domain unchanged)if__name____main__:main()tools/quality.py本地与CI复用的质量门禁任一检查失败立即停止。frompathlibimportPathimportosimportsubprocessimportsys ROOTPath(__file__).resolve().parents[1]PROJECTROOT/projectdefmain():commands[[-m,ruff,check,src,tests],[-m,ruff,format,--check,src,tests],[-m,mypy,src],[-m,pytest,-q,--covquoteledger,--cov-branch,--cov-reportterm-missing,--cov-reportjson:../reports/coverage.json,--cov-fail-under85],]envos.environ.copy()env[PYTHONUTF8]1(ROOT/reports).mkdir(exist_okTrue)forcommandincommands:print(python .join(command),flushTrue)resultsubprocess.run([sys.executable,*command],cwdPROJECT,envenv)ifresult.returncode:raiseSystemExit(result.returncode)print(All quality gates passed)if__name____main__:main()project/pyproject.toml[build-system] requires [setuptools75] build-backend setuptools.build_meta [project] name quoteledger version 1.0.0 description 报价单工程化、测试与重构课程项目 readme README.md requires-python 3.11 dependencies [fastapi0.115.12, pydantic2.13.5, uvicorn0.34.2] [project.optional-dependencies] dev [pytest8.3.5, httpx0.28.1, pytest-cov6.1.1, hypothesis6.131.9, ruff0.11.13, mypy1.15.0, build1.2.2.post1] [tool.setuptools.packages.find] where [src] [tool.pytest.ini_options] testpaths [tests] [tool.ruff] target-version py311 line-length 88 [tool.ruff.lint] select [E4, E7, E9, F, I] [tool.mypy] python_version 3.11 strict true files [src] [tool.coverage.run] branch true source [quoteledger] [tool.coverage.report] show_missing true fail_under 85

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询