
第 5 章 编码与单元测试学习目标理解编码是设计的精确化这一原则掌握编码规范与可读性实践理解重构(refactoring)及其安全网(自动化测试)掌握单元测试的基本原则(Arrange-Act-Assert、隔离、可重复)理解 TDD(测试驱动开发)的红-绿-重构循环了解代码覆盖率与静态分析掌握代码评审(Code Review)的要点5.1 编码的定位编码不是软件工程的主体,而是把设计精确化为机器可执行形式;但代码是最终交付物,也是缺陷的聚集地,因此编码质量直接决定维护成本;两条纪律:代码即文档——好代码自解释,注释解释为什么而非是什么;先有安全网,再做修改——没有测试保护的代码,任何重构都是赌博。编码阶段的输入-输出输入输出质量判据详细设计(接口契约、类图)可编译代码与契约一致,无顺手改设计编码规范(团队)风格一致代码Linter 全绿单元测试策略单测套件行为级断言,AAA 结构,CI 全绿设计中的边界/异常分析边界处理与错误码异常流有实现且有测试顺手改设计的治理:编码中发现设计问题 → 不走私改通道,走轻量 CR(影响小)或正式变更(影响大);任何改动同步更新设计工件。这是编码是精确化纪律的配套机制。5.2 编码规范与可读性命名(最重要的可读性)对象约定反例 → 正例变量名词,表达含义而非类型d→orderDeadline函数动词短语,名实相符process()→calculateRefundAmount()类名词,职责单一Manager2→InventoryReservationService布尔is/has/should/can 前缀flag→isPaid常量全大写/语义名100→FREE_SHIPPING_THRESHOLD集合复数 Of 语义list→pendingOrders命名自检:能否不看实现,仅凭名字推断行为?能否用名字直接写进测试名(test_已发货订单不可退款)?其他要点函数短小:一个函数只做一件事,建议 30 行、参数 ≤4;避免深嵌套:用卫语句(guard clause)提前返回;魔法数字命名化:if (amount 100)→if (amount FREE_SHIPPING_THRESHOLD);不吞异常:至少记录上下文,或转换为领域异常;注释解释意图与约束,而非复述代码;团队统一规范 工具强制:风格检查器(ESLint/PMD/Checkstyle)在 CI 中运行,不靠人盯。注释的三分法类型例子价值意图/背景(该写)“支付网关要求金额单位为分,此处乘 100”防止后人’优化’掉约束/陷阱(该写)“不可改顺序:释放锁必须在回调之后”防回归复述代码(不该写)// 加 1配x噪音,且会过期规范落地机制:规范文档 1 页(超出说明没提炼);规则尽量编码进 Linter/格式化器(自动 人工);例外走豁免注释 理由(如// lint-override: 性能热点,见 PERF-12);规范本身进版本库,变更走评审。5.3 重构(Refactoring)重构:在不改变外部可观察行为的前提下,改善代码内部结构。常见重构手法手法场景例提取函数函数过长/职责混杂把计算运费从下单函数中抽出提取类一个类职责过多(SRP)Order拆出OrderPricing引入参数对象参数过多(userId, addr, payMethod)→PlaceOrderCmd以多态替换条件if-else/switch 随类型膨胀(OCP)三种运费算法 → 三个PricingStrategy内联/替换算法简化——移除死代码长期无用——重构候选信号(代码坏味道):长函数/长参数表/大类;重复代码(同一逻辑三处以上);if-else 链随枚举值膨胀;频繁修改某处导致反复回归;名字与行为不符(名不副实是最隐蔽的坏味道)。重构的安全网先有充分的自动化测试(至少覆盖要改动的行为);小步进行:每次只做一个重构,立即跑测试;提交粒度小:每个重构一个 commit,便于回退与审查;禁止重构 行为修改混在同一 commit。重构窗口管理重构 vs 重写:重写是从零开始,风险高、周期长;重构是持续小步改进,配合 TDD 可长期演进。OOS 在双 11 前冻结重构窗口,大促后集中 2 周偿还技术债——重构不是随时可做,它也是风险。持续小重构:日常合入中允许顺手小重构(≤ 一个文件,测试保护);窗口式大重构:结构性重构(跨模块)集中到稳定窗口,避免与功能开发并行造成双线风险;冻结期:大促前 2 周、重大发布前 3 天,禁止非缺陷修复的重构;判据:重构投入产出 未来修改成本下降 × 修改频率,低于阈值的重构不做(不是所有坏味道都值得现在修)。5.4 单元测试(Unit Test)定义与边界单元测试:针对最小可测单元(函数/类/模块)的自动化测试,验证其在隔离环境下的行为。边界判断:测行为(输入 → 输出/状态),不测实现细节;测边界与异常(空、极值、非法输入、并发),不只测正常路径;一个测试只验证一个行为点(失败时能立即定位)。AAA 结构(Arrange-Act-Assert)deftest_refund_not_allowed_after_shipped():# Arrange: 准备状态orderOrder(statusSHIPPED,amount100)serviceRefundService(inventorymock_inventory)# Act: 执行被测行为resultservice.request_refund(order.id)# Assert: 断言结果assertresult.rejectedassertorder.statusSHIPPED# 状态未变单元隔离:Mock / Stub / Spy替身用途OOS 例Stub(桩)提供固定返回值让PaymentGateway返回支付成功Mock(模拟)断言是否被以正确参数调用断言下单后确实调用了inventory.reserve(sku, qty)Spy(间谍)记录调用,可带真实逻辑记录邮件发送次数与收件人Mock 是隔离手段,不是设计目的。过度 mock 会测出实现的镜像,重构时全崩。优先测公共契约,少测私有实现。Mock 边界判断(三问):这个依赖在单元测试中是否昂贵/不稳定(网络、时间、随机)?是 → 替身;我断言的是行为结果还是调用细节?调用细节 → 警惕过度;重构该依赖的内部实现时,测试是否需要改?需要 → 测太深了。替代手段优先:能用构造注入的简单对象就不 mock(如用内存版库存仓储);能注入假时钟就不 mock 时间。覆盖率(Coverage)行覆盖:执行到的行比例;分支覆盖:执行到的 if/else 分支比例(更有意义);条件覆盖 / MC/DC:每个布尔条件及其组合(安全关键领域)。定位:覆盖率是测试是否充分的下限指标,不是目标。100% 行覆盖不等于测对了行为。覆盖率的正确使用:门禁用增量覆盖率(新代码 ≥80%),存量逐步抬升;看未覆盖清单做风险判断(未覆盖的是死代码还是高危分支?);覆盖率下降必须在 PR 中说明理由;永远与缺陷数据对照:高覆盖模块若仍频繁出缺陷,说明测的是行为盲区。测试命名与组织命名即文档:test_场景_条件_期望(如test_满减_恰好100元_减20);同一行为族放同一文件/类,用分组注释标注边界;测试数据用工厂/构造器集中管理,不散落在用例里;禁止测试间共享可变状态(顺序依赖 flaky 之源)。5.5 测试驱动开发(TDD)红-绿-重构循环:1. 红(Red) : 先写一个会失败的测试(描述期望行为) 2. 绿(Green) : 写最少的代码让测试通过 3. 重构(Refactor): 在测试保护下改善结构 ───────────────────────────────────────── 回到 1,进入下一个行为每一步的停止条件:红:测试因功能未实现失败,而非写错了测试(编译/导入错误不算红);绿:通过,且没有引入超出当前行为的代码(YAGNI);重构:测试仍绿,结构有可命名的改善;无改善则直接进入下一个红。TDD 的收益测试与实现同步产生,无先写完再补测试的滞后;倒逼接口设计:写测试时被迫思考这个 API 用起来舒服吗;小步实现,天然符合增量;心理安全:随时重构。TDD 的边界不是所有代码都适合 TDD:UI、探索性原型、一次性脚本;TDD 不解决测什么——测错行为同样红绿循环,仍产出错误系统;与 BDD(行为驱动开发)互补:BDD 用 Gherkin 语法(给定/当/那么)写行为验收测试,弥补 TDD测实现的倾向。BDD 示例(OOS 退款)场景: 已发货订单不可直接退款,需走退货流程 给定 订单 ORD-123 状态为已发货,实付 100 元 当 用户点击申请退款 那么 系统提示已发货订单请先申请退货 并且 订单状态保持已发货不变BDD 协作价值:Given/When/Then 是产品-开发-测试三方签字的行为契约,PO 能读懂、测试能执行、开发能实现,一份描述三种用途。5.6 代码评审(Code Review)要点说明规模单次 ≤ 400 行 / 30 分钟内可完成,超则拆分分工评审者找问题,作者答疑问;作者不自审关注正确性 安全性 可维护性 风格(风格交给 Linter)语气对事不对人;评论绑定代码行;区分必须改/建议/疑问时效24 小时内响应,评审是流水线的一部分度量评审缺陷发现率(非零为健康)、平均响应时长评审者检查清单:行为与设计/需求一致?边界与异常都处理了?有没有顺手改了设计而未同步工件?命名与函数职责是否名实相符?错误处理:吞异常、错误码混用、日志缺上下文?安全:注入点、越权、敏感数据进日志?测试:新增行为有测试吗?测的是行为还是实现?5.7 静态分析与工具链工具类别作用典型工具静态分析(lint)风格、潜在缺陷、复杂度ESLint、PMD、SonarQube类型检查编译期捕捉类型错误TypeScript、Mypy依赖审计已知漏洞、许可证合规Snyk、Dependabot圈复杂度(Cyclomatic)量化分支复杂度,15 预警各静态分析器内置CI 集成:提交 → 自动跑 lint 单测 覆盖率门禁(如分支覆盖 ≥80%)→ 不通过则禁止合入。门禁分级(建议):阻断级:编译失败、单测失败、安全高危、增量覆盖率 80%;警告级:圈复杂度超标、技术债标记、风格问题(累计超阈值转阻断);报告级:覆盖率趋势、重复率、依赖漏洞清单(进周报)。5.8 编码阶段的反模式反模式症状对策复制粘贴编程同一段逻辑三处以上坏味道清单(5.3)触发提取注释式废弃大段注释代码舍不得删版本库是垃圾桶:删掉,历史可查魔法配置硬编码散落各处配置集中 命名常量防御性编程泛滥到处判空、到处 catch边界处校验,内部信任契约(快速失败)补测试表演事后写只测正常路径的测试评审检查异常流;增量覆盖率门禁评审橡皮图章1 分钟点 Approve评审规模控制 发现率度量5.9 本章 FAQQ1:单测写多少算够?没有绝对数;判据是该单元的行为空间被有意义地覆盖:主路径 所有边界 所有声明的异常 并发(如适用)。用增量覆盖率兜底,用缺陷分布修正。Q2:遗留代码没有测试,如何开始重构?先做特征测试(characterization test):录制现有输入输出作为基线(先别判断对错),重构时以基线不变为准;同时与业务确认哪些行为本来就是错的,把错的基线显式标记。Q3:AI 生成的代码如何验证?把 AI 产出当外部贡献者代码:同样的评审 同样的测试要求,外加理解责任——你无法解释的生成代码不予合入。生成速度提升,验证与责任权重上升(第 1 章)。Q4:TDD 对团队协作意味着什么?意义不在个人效率,而在契约同步:测试是可执行的需求,团队对同一行为的理解在写代码前就被迫对齐,评审时争议减半。Q5:如何避免测试写多了反而拖慢开发?先诊断慢在哪一环:构建慢(缓存/并行,第 11 章)、测试执行慢(分层:先跑受影响的)、还是写测试慢(往往是无测设计——接口耦合、依赖具体实现,根因在设计而非测试)。测试慢多数时候是设计问题的症状,TDD 与依赖注入就是解药。把测试拖慢开发当口号而不做归因,等于用症状掩盖根因。Q6:单元测试与集成测试的边界到底在哪?判据:被验证的行为是否跨模块边界。模块内部逻辑 → 单元;模块间协作(接口语义、数据流动、失败传递) → 集成。常见错误:单元测试里连了真实数据库或起真实容器(它其实是集成测试,却享受了单元的速度与隔离假设);另一个极端:把所有协作行为都推到 E2E,集成层空洞。用这个行为换一个实现还能不能复用这个测试来检验:能 → 测的是契约(好);不能 → 测到了实现(警惕)。Q7:代码烂到不敢动怎么处理?四步:① 特征测试兜底(FAQ Q2),先锁定现状行为;② 不整体重构,切香肠——挑一两个高频改动点先改善;③ 设退出判据(覆盖率 X% / 模块缺陷数降 Y%);④ 若性价比不成立,评估弃用/替换而非拯救(第 10 章 EOL)。最大风险是永远在救火的无限循环——没有退出判据的代码改善是慢性失血。附录:编码阶段迭代检查单每个迭代结束逐项检查(可作为回顾会的质量环节议程):#项判定1CI 全绿,主干健康(无挂起的红)是/否2增量覆盖率 ≥80%,下降有说明数值3静态分析高危问题数数值(目标 0)4代码评审平均响应时长≤24h5评审缺陷发现率(条/PR)非零且稳定6重构 commit 与行为变更分离是/否7以后再说项全部进技术债登记册是/否8有无改了代码没更新设计工件无9命名/异常/错误码符合规范抽查是/否10flaky 测试数量0任一否→ 产生改进项(Owner 期限),不允许以下次注意关闭。检查单本身每半年审视一次:连续两次全绿的项可降级为抽查,防止检查单通胀(第 9 章度量纪律同理)。5.10 小结编码是设计的精确化;输入是契约,输出是与契约一致 有测试保护;可读性 聪明,命名是第一生产力;规范要工具化;重构 不改行为改善结构,前提是自动化测试安全网 小步 单提交 窗口管理;单元测试:测行为不测实现,AAA 结构,Mock 三问边界,覆盖率是下限非目标;TDD 红-绿-重构各有停止条件;BDD 用业务语言补行为契约;代码评审是流水线环节:规模控制 清单 发现率度量;静态分析 类型检查 依赖审计进 CI,门禁分级。思考题重构 修 bug能否放在同一个 commit?为什么?Mock 过度使用的典型症状是什么?如何在团队中约束?100% 行覆盖的系统仍可能在生产崩溃,给出 3 个具体例子。为库存预扣功能写出 3 个单元测试用例(含边界与异常),并用 AAA 结构描述。用 5.3 的重构候选信号扫描你的代码库,列出 3 个候选重构及其未来修改成本下降 × 修改频率的估算。设计你团队的代码评审规范:规模上限、响应时效、评论分级、度量指标各定什么?为什么?对一段没有测试的遗留模块,写出你的特征测试 → 重构分步计划(至少 5 步,含风险控制点)。门禁分级(阻断/警告/报告)中,为什么风格问题不直接阻断?给出一个警告转阻断的触发规则。