news 2026/10/11 1:42:35

05-编码与单元测试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
05-编码与单元测试

第 5 章 编码与单元测试

学习目标

  • 理解"编码是设计的精确化"这一原则
  • 掌握编码规范与可读性实践
  • 理解重构(refactoring)及其安全网(自动化测试)
  • 掌握单元测试的基本原则(Arrange-Act-Assert、隔离、可重复)
  • 理解 TDD(测试驱动开发)的红-绿-重构循环
  • 了解代码覆盖率与静态分析
  • 掌握代码评审(Code Review)的要点

5.1 编码的定位

  • 编码不是软件工程的主体,而是把设计精确化为机器可执行形式;
  • 但代码是最终交付物,也是缺陷的聚集地,因此编码质量直接决定维护成本;
  • 两条纪律:
    1. 代码即文档——好代码自解释,注释解释"为什么"而非"是什么";
    2. 先有安全网,再做修改——没有测试保护的代码,任何重构都是赌博。

编码阶段的输入-输出

输入输出质量判据
详细设计(接口契约、类图)可编译代码与契约一致,无"顺手改设计"
编码规范(团队)风格一致代码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. 规范文档 < 1 页(超出说明没提炼);
  2. 规则尽量编码进 Linter/格式化器(自动 > 人工);
  3. 例外走"豁免注释 + 理由"(如// lint-override: 性能热点,见 PERF-12);
  4. 规范本身进版本库,变更走评审。

5.3 重构(Refactoring)

重构:在不改变外部可观察行为的前提下,改善代码内部结构。

常见重构手法

手法场景例
提取函数函数过长/职责混杂把"计算运费"从下单函数中抽出
提取类一个类职责过多(SRP)Order拆出OrderPricing
引入参数对象参数过多(userId, addr, payMethod)→PlaceOrderCmd
以多态替换条件if-else/switch 随类型膨胀(OCP)三种运费算法 → 三个PricingStrategy
内联/替换算法简化——
移除死代码长期无用——

重构候选信号(代码坏味道):

  • 长函数/长参数表/大类;
  • 重复代码(同一逻辑三处以上);
  • if-else 链随枚举值膨胀;
  • 频繁修改某处导致反复回归;
  • 名字与行为不符(名不副实是最隐蔽的坏味道)。

重构的安全网

  1. 先有充分的自动化测试(至少覆盖要改动的行为);
  2. 小步进行:每次只做一个重构,立即跑测试;
  3. 提交粒度小:每个重构一个 commit,便于回退与审查;
  4. 禁止"重构 + 行为修改"混在同一 commit。

重构窗口管理

重构 vs 重写:重写是从零开始,风险高、周期长;重构是持续小步改进,配合 TDD 可长期演进。OOS 在双 11 前冻结重构窗口,大促后集中 2 周偿还技术债——重构不是随时可做,它也是风险。

  • 持续小重构:日常合入中允许"顺手小重构"(≤ 一个文件,测试保护);
  • 窗口式大重构:结构性重构(跨模块)集中到稳定窗口,避免与功能开发并行造成双线风险;
  • 冻结期:大促前 2 周、重大发布前 3 天,禁止非缺陷修复的重构;
  • 判据:重构投入产出 = 未来修改成本下降 × 修改频率,低于阈值的重构不做(不是所有坏味道都值得现在修)。

5.4 单元测试(Unit Test)

定义与边界

单元测试:针对最小可测单元(函数/类/模块)的自动化测试,验证其在隔离环境下的行为。

边界判断:

  • 测行为(输入 → 输出/状态),不测实现细节;
  • 测边界与异常(空、极值、非法输入、并发),不只测"正常路径";
  • 一个测试只验证一个行为点(失败时能立即定位)。

AAA 结构(Arrange-Act-Assert)

deftest_refund_not_allowed_after_shipped():# Arrange: 准备状态order=Order(status=SHIPPED,amount=100)service=RefundService(inventory=mock_inventory)# Act: 执行被测行为result=service.request_refund(order.id)# Assert: 断言结果assertresult.rejectedassertorder.status==SHIPPED# 状态未变

单元隔离:Mock / Stub / Spy

替身用途OOS 例
Stub(桩)提供固定返回值让PaymentGateway返回"支付成功"
Mock(模拟)断言"是否被以正确参数调用"断言下单后确实调用了inventory.reserve(sku, qty)
Spy(间谍)记录调用,可带真实逻辑记录邮件发送次数与收件人

Mock 是隔离手段,不是设计目的。过度 mock 会测出"实现的镜像",重构时全崩。优先测公共契约,少测私有实现。

Mock 边界判断(三问):

  1. 这个依赖在单元测试中是否昂贵/不稳定(网络、时间、随机)?是 → 替身;
  2. 我断言的是"行为结果"还是"调用细节"?调用细节 → 警惕过度;
  3. 重构该依赖的内部实现时,测试是否需要改?需要 → 测太深了。

替代手段优先:能用构造注入的简单对象就不 mock(如用内存版库存仓储);能注入假时钟就不 mock 时间。

覆盖率(Coverage)

  • 行覆盖:执行到的行比例;
  • 分支覆盖:执行到的 if/else 分支比例(更有意义);
  • 条件覆盖 / MC/DC:每个布尔条件及其组合(安全关键领域)。

定位:覆盖率是"测试是否充分"的下限指标,不是目标。100% 行覆盖不等于"测对了行为"。

覆盖率的正确使用:

  1. 门禁用增量覆盖率(新代码 ≥80%),存量逐步抬升;
  2. 看未覆盖清单做风险判断(未覆盖的是死代码还是高危分支?);
  3. 覆盖率下降必须在 PR 中说明理由;
  4. 永远与缺陷数据对照:高覆盖模块若仍频繁出缺陷,说明测的是行为盲区。

测试命名与组织

  • 命名即文档: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 小时内响应,评审是流水线的一部分
度量评审缺陷发现率(非零为健康)、平均响应时长

评审者检查清单:

  1. 行为与设计/需求一致?边界与异常都处理了?
  2. 有没有"顺手改了设计"而未同步工件?
  3. 命名与函数职责是否名实相符?
  4. 错误处理:吞异常、错误码混用、日志缺上下文?
  5. 安全:注入点、越权、敏感数据进日志?
  6. 测试:新增行为有测试吗?测的是行为还是实现?

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 本章 FAQ

Q1:单测写多少算够?
没有绝对数;判据是"该单元的行为空间被有意义地覆盖":主路径 + 所有边界 + 所有声明的异常 + 并发(如适用)。用增量覆盖率兜底,用缺陷分布修正。

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代码评审平均响应时长≤24h
5评审缺陷发现率(条/PR)非零且稳定
6重构 commit 与行为变更分离是/否
7"以后再说"项全部进技术债登记册是/否
8有无"改了代码没更新设计工件"无
9命名/异常/错误码符合规范抽查是/否
10flaky 测试数量=0

任一"否"→ 产生改进项(Owner + 期限),不允许以"下次注意"关闭。检查单本身每半年审视一次:连续两次全绿的项可降级为抽查,防止检查单通胀(第 9 章度量纪律同理)。

5.10 小结

  • 编码是设计的精确化;输入是契约,输出是"与契约一致 + 有测试保护";
  • 可读性 > 聪明,命名是第一生产力;规范要工具化;
  • 重构 = 不改行为改善结构,前提是自动化测试安全网 + 小步 + 单提交 + 窗口管理;
  • 单元测试:测行为不测实现,AAA 结构,Mock 三问边界,覆盖率是下限非目标;
  • TDD 红-绿-重构各有停止条件;BDD 用业务语言补行为契约;
  • 代码评审是流水线环节:规模控制 + 清单 + 发现率度量;
  • 静态分析 + 类型检查 + 依赖审计进 CI,门禁分级。

思考题

  1. "重构 + 修 bug"能否放在同一个 commit?为什么?
  2. Mock 过度使用的典型症状是什么?如何在团队中约束?
  3. 100% 行覆盖的系统仍可能在生产崩溃,给出 3 个具体例子。
  4. 为"库存预扣"功能写出 3 个单元测试用例(含边界与异常),并用 AAA 结构描述。
  5. 用 5.3 的"重构候选信号"扫描你的代码库,列出 3 个候选重构及其"未来修改成本下降 × 修改频率"的估算。
  6. 设计你团队的代码评审规范:规模上限、响应时效、评论分级、度量指标各定什么?为什么?
  7. 对一段没有测试的遗留模块,写出你的"特征测试 → 重构"分步计划(至少 5 步,含风险控制点)。
  8. 门禁分级(阻断/警告/报告)中,为什么"风格问题"不直接阻断?给出一个"警告转阻断"的触发规则。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 1:42:22

AI Agent进入生产后,如何做到可观测、可评估、可运营?

导读 本文整理自云器科技技术专家蔡瀛在 DataFun 云器科技直播中的分享。随着 AI Agent 从 Demo 进入真实业务&#xff0c;团队需要回答的问题已经从“能不能跑”变成“任务到底有没有做对”。围绕这一问题&#xff0c;云器科技介绍了 SingSight 的产品设计、生产环境中的持续…

作者头像 李华
网站建设 2026/10/11 1:42:02

用Python做股票价格序列相似性分析:DTW与形态匹配实践

简介&#xff1a;基于Python的股票价格序列相似性分析课程设计资源包&#xff0c;面向金融数据分析、Python编程及算法课程设计人群&#xff0c;围绕动态时间弯曲&#xff08;DTW&#xff09;算法实现股票价格序列的相似性度量&#xff0c;并通过折线图直观呈现对比结果&#x…

作者头像 李华
网站建设 2026/10/11 1:41:40

基于STM32单片机超声波雷达测距仪雷达扫描视频监控温补蓝牙/WiFi/视频监控/云平台无线APP-DIY设计S468

S468-超声波雷达动态扫描舵机温度补偿调整方向启动停止测距报警频率变化扫描范围OLED屏声光提醒按键蓝牙/WiFi/视频监控/云平台APP本系统由STM32F103C8T6单片机核心板、OLED屏、无线蓝牙/WIFI/视频监控/云平台模块-可选、超声波模块、温度补偿检测电路、舵机控制电路、蜂鸣器报…

作者头像 李华
网站建设 2026/10/11 1:41:01

YOLOv11边缘部署实战:从TensorRT转换到性能调优的完整链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华