📌目录
- [🧪 单元测试:保障代码质量的第一道防线](#🧪 单元测试:保障代码质量的第一道防线)
-
- [🎯 一、单元测试概述](#🎯 一、单元测试概述)
- [📦 二、单元测试的设计原则](#📦 二、单元测试的设计原则)
-
- [(一)AIR 原则](#(一)AIR 原则)
- [(二)BCDE 原则](#(二)BCDE 原则)
- [(三)FIRM 原则](#(三)FIRM 原则)
- [🔧 三、单元测试的方法与技术](#🔧 三、单元测试的方法与技术)
- [🔄 四、TDD 测试驱动开发](#🔄 四、TDD 测试驱动开发)
-
- [(一)TDD 概述](#(一)TDD 概述)
- [(二)TDD 的优势与挑战](#(二)TDD 的优势与挑战)
- [(三)TDD 与传统开发对比](#(三)TDD 与传统开发对比)
- [📊 五、单元测试的 Mock 与 Stub](#📊 五、单元测试的 Mock 与 Stub)
-
- [(一)为什么需要 Mock](#(一)为什么需要 Mock)
- [(二)Test Double 分类](#(二)Test Double 分类)
- [(三)Mock 使用示例](#(三)Mock 使用示例)
- [📈 六、单元测试的覆盖率](#📈 六、单元测试的覆盖率)
- [📝 七、单元测试的最佳实践与注意事项](#📝 七、单元测试的最佳实践与注意事项)
-
- (一)常见问题
- [(二)AAA 模式](#(二)AAA 模式)
- (三)最佳实践
- (四)单元测试的检查清单
- [📝 总结](#📝 总结)

🧪 单元测试:保障代码质量的第一道防线
单元测试是软件开发过程中最基础、最重要的测试层次。它针对软件中的最小可测试单元(通常是函数、方法或类)进行检查和验证,确保每个独立模块的行为符合预期。本文将详细介绍单元测试的概念、原则、设计方法、工具和最佳实践。

🎯 一、单元测试概述
(一)单元测试的定义
单元测试(Unit Testing)是对软件中的最小可测试单元进行检验的测试方法。所谓"单元",是指软件中不可再分的最小功能模块,通常是一个函数、一个方法或一个类。单元测试的目标是验证每个单元在各种输入条件下的行为是否正确。
单元测试概念:
#mermaid-svg-SuOFnmkDTLtMcdcN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SuOFnmkDTLtMcdcN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SuOFnmkDTLtMcdcN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SuOFnmkDTLtMcdcN .error-icon{fill:#552222;}#mermaid-svg-SuOFnmkDTLtMcdcN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SuOFnmkDTLtMcdcN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SuOFnmkDTLtMcdcN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SuOFnmkDTLtMcdcN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SuOFnmkDTLtMcdcN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SuOFnmkDTLtMcdcN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SuOFnmkDTLtMcdcN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SuOFnmkDTLtMcdcN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SuOFnmkDTLtMcdcN .marker.cross{stroke:#333333;}#mermaid-svg-SuOFnmkDTLtMcdcN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SuOFnmkDTLtMcdcN p{margin:0;}#mermaid-svg-SuOFnmkDTLtMcdcN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SuOFnmkDTLtMcdcN .cluster-label text{fill:#333;}#mermaid-svg-SuOFnmkDTLtMcdcN .cluster-label span{color:#333;}#mermaid-svg-SuOFnmkDTLtMcdcN .cluster-label span p{background-color:transparent;}#mermaid-svg-SuOFnmkDTLtMcdcN .label text,#mermaid-svg-SuOFnmkDTLtMcdcN span{fill:#333;color:#333;}#mermaid-svg-SuOFnmkDTLtMcdcN .node rect,#mermaid-svg-SuOFnmkDTLtMcdcN .node circle,#mermaid-svg-SuOFnmkDTLtMcdcN .node ellipse,#mermaid-svg-SuOFnmkDTLtMcdcN .node polygon,#mermaid-svg-SuOFnmkDTLtMcdcN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SuOFnmkDTLtMcdcN .rough-node .label text,#mermaid-svg-SuOFnmkDTLtMcdcN .node .label text,#mermaid-svg-SuOFnmkDTLtMcdcN .image-shape .label,#mermaid-svg-SuOFnmkDTLtMcdcN .icon-shape .label{text-anchor:middle;}#mermaid-svg-SuOFnmkDTLtMcdcN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SuOFnmkDTLtMcdcN .rough-node .label,#mermaid-svg-SuOFnmkDTLtMcdcN .node .label,#mermaid-svg-SuOFnmkDTLtMcdcN .image-shape .label,#mermaid-svg-SuOFnmkDTLtMcdcN .icon-shape .label{text-align:center;}#mermaid-svg-SuOFnmkDTLtMcdcN .node.clickable{cursor:pointer;}#mermaid-svg-SuOFnmkDTLtMcdcN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SuOFnmkDTLtMcdcN .arrowheadPath{fill:#333333;}#mermaid-svg-SuOFnmkDTLtMcdcN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SuOFnmkDTLtMcdcN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SuOFnmkDTLtMcdcN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SuOFnmkDTLtMcdcN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SuOFnmkDTLtMcdcN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SuOFnmkDTLtMcdcN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SuOFnmkDTLtMcdcN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SuOFnmkDTLtMcdcN .cluster text{fill:#333;}#mermaid-svg-SuOFnmkDTLtMcdcN .cluster span{color:#333;}#mermaid-svg-SuOFnmkDTLtMcdcN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SuOFnmkDTLtMcdcN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SuOFnmkDTLtMcdcN rect.text{fill:none;stroke-width:0;}#mermaid-svg-SuOFnmkDTLtMcdcN .icon-shape,#mermaid-svg-SuOFnmkDTLtMcdcN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SuOFnmkDTLtMcdcN .icon-shape p,#mermaid-svg-SuOFnmkDTLtMcdcN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SuOFnmkDTLtMcdcN .icon-shape .label rect,#mermaid-svg-SuOFnmkDTLtMcdcN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SuOFnmkDTLtMcdcN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SuOFnmkDTLtMcdcN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SuOFnmkDTLtMcdcN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 单元测试
最小测试单元
独立验证
代码级测试
白盒测试
函数/方法
类/模块
隔离测试
不依赖外部环境
开发者编写
与代码同步
覆盖代码路径
逻辑验证
核心理念:
| 问题 | 说明 |
|---|---|
| 测什么? | 最小可测试单元(函数、方法、类) |
| 谁来测? | 通常由开发人员编写和执行 |
| 怎么测? | 编写测试代码,调用被测单元,验证输出 |
| 何时测? | 编码阶段,通常与开发同步进行 |
| 为什么测? | 尽早发现代码缺陷,保障代码质量 |
(二)单元测试的目的
测试目的:
#mermaid-svg-NSDqbzcadm0djTIb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NSDqbzcadm0djTIb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NSDqbzcadm0djTIb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NSDqbzcadm0djTIb .error-icon{fill:#552222;}#mermaid-svg-NSDqbzcadm0djTIb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NSDqbzcadm0djTIb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NSDqbzcadm0djTIb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NSDqbzcadm0djTIb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NSDqbzcadm0djTIb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NSDqbzcadm0djTIb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NSDqbzcadm0djTIb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NSDqbzcadm0djTIb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NSDqbzcadm0djTIb .marker.cross{stroke:#333333;}#mermaid-svg-NSDqbzcadm0djTIb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NSDqbzcadm0djTIb p{margin:0;}#mermaid-svg-NSDqbzcadm0djTIb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NSDqbzcadm0djTIb .cluster-label text{fill:#333;}#mermaid-svg-NSDqbzcadm0djTIb .cluster-label span{color:#333;}#mermaid-svg-NSDqbzcadm0djTIb .cluster-label span p{background-color:transparent;}#mermaid-svg-NSDqbzcadm0djTIb .label text,#mermaid-svg-NSDqbzcadm0djTIb span{fill:#333;color:#333;}#mermaid-svg-NSDqbzcadm0djTIb .node rect,#mermaid-svg-NSDqbzcadm0djTIb .node circle,#mermaid-svg-NSDqbzcadm0djTIb .node ellipse,#mermaid-svg-NSDqbzcadm0djTIb .node polygon,#mermaid-svg-NSDqbzcadm0djTIb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NSDqbzcadm0djTIb .rough-node .label text,#mermaid-svg-NSDqbzcadm0djTIb .node .label text,#mermaid-svg-NSDqbzcadm0djTIb .image-shape .label,#mermaid-svg-NSDqbzcadm0djTIb .icon-shape .label{text-anchor:middle;}#mermaid-svg-NSDqbzcadm0djTIb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NSDqbzcadm0djTIb .rough-node .label,#mermaid-svg-NSDqbzcadm0djTIb .node .label,#mermaid-svg-NSDqbzcadm0djTIb .image-shape .label,#mermaid-svg-NSDqbzcadm0djTIb .icon-shape .label{text-align:center;}#mermaid-svg-NSDqbzcadm0djTIb .node.clickable{cursor:pointer;}#mermaid-svg-NSDqbzcadm0djTIb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NSDqbzcadm0djTIb .arrowheadPath{fill:#333333;}#mermaid-svg-NSDqbzcadm0djTIb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NSDqbzcadm0djTIb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NSDqbzcadm0djTIb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NSDqbzcadm0djTIb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NSDqbzcadm0djTIb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NSDqbzcadm0djTIb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NSDqbzcadm0djTIb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NSDqbzcadm0djTIb .cluster text{fill:#333;}#mermaid-svg-NSDqbzcadm0djTIb .cluster span{color:#333;}#mermaid-svg-NSDqbzcadm0djTIb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NSDqbzcadm0djTIb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NSDqbzcadm0djTIb rect.text{fill:none;stroke-width:0;}#mermaid-svg-NSDqbzcadm0djTIb .icon-shape,#mermaid-svg-NSDqbzcadm0djTIb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NSDqbzcadm0djTIb .icon-shape p,#mermaid-svg-NSDqbzcadm0djTIb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NSDqbzcadm0djTIb .icon-shape .label rect,#mermaid-svg-NSDqbzcadm0djTIb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NSDqbzcadm0djTIb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NSDqbzcadm0djTIb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NSDqbzcadm0djTIb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 单元测试目的
验证正确性
快速定位缺陷
代码重构保障
文档化设计
逻辑正确
边界处理
缩小排查范围
问题隔离
安全修改代码
防止回归
展示接口用法
约束行为规范
目的详解:
| 目的 | 说明 |
|---|---|
| 验证代码正确性 | 确保每个单元的功能符合设计预期 |
| 快速定位缺陷 | 单元级别测试可以精确定位问题代码 |
| 支持安全重构 | 完善的单元测试是重构代码的安全网 |
| 防止回归 | 代码修改后通过回归测试确保不影响已有功能 |
| 文档化设计 | 测试用例本身是对代码接口的活文档 |
| 促进设计改进 | 编写可测试的代码倒逼良好的设计实践 |
(三)单元测试的特点
测试特点:
| 特点 | 说明 | 实践意义 |
|---|---|---|
| 原子性 | 每个测试用例只验证一个行为 | 失败时能立即定位问题 |
| 独立性 | 测试用例之间互不依赖 | 可以单独运行任意用例 |
| 可重复性 | 每次执行结果一致 | 不依赖外部状态或随机因素 |
| 快速性 | 执行速度快,通常毫秒级 | 可频繁运行,不影响开发效率 |
| 自动化 | 完全由代码驱动,无需人工干预 | 可集成到CI/CD流水线 |
| 隔离性 | 不依赖数据库、网络等外部资源 | 使用Mock替代外部依赖 |
💡 关键理解:单元测试的核心思想是"隔离"------把被测单元与它的依赖完全隔离开来,单独验证它自身的逻辑。就像一个零件在出厂前要单独做质检,而不是装到整台机器上才能检查。单元测试是测试金字塔的基座:数量最多、执行最快、发现问题最早。
📦 二、单元测试的设计原则
(一)AIR 原则
AIR原则是单元测试必须遵循的三个基本原则:
AIR原则内容:
#mermaid-svg-1okPqkMxkgV0qmk6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1okPqkMxkgV0qmk6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1okPqkMxkgV0qmk6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1okPqkMxkgV0qmk6 .error-icon{fill:#552222;}#mermaid-svg-1okPqkMxkgV0qmk6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1okPqkMxkgV0qmk6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1okPqkMxkgV0qmk6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1okPqkMxkgV0qmk6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1okPqkMxkgV0qmk6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1okPqkMxkgV0qmk6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1okPqkMxkgV0qmk6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1okPqkMxkgV0qmk6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1okPqkMxkgV0qmk6 .marker.cross{stroke:#333333;}#mermaid-svg-1okPqkMxkgV0qmk6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1okPqkMxkgV0qmk6 p{margin:0;}#mermaid-svg-1okPqkMxkgV0qmk6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1okPqkMxkgV0qmk6 .cluster-label text{fill:#333;}#mermaid-svg-1okPqkMxkgV0qmk6 .cluster-label span{color:#333;}#mermaid-svg-1okPqkMxkgV0qmk6 .cluster-label span p{background-color:transparent;}#mermaid-svg-1okPqkMxkgV0qmk6 .label text,#mermaid-svg-1okPqkMxkgV0qmk6 span{fill:#333;color:#333;}#mermaid-svg-1okPqkMxkgV0qmk6 .node rect,#mermaid-svg-1okPqkMxkgV0qmk6 .node circle,#mermaid-svg-1okPqkMxkgV0qmk6 .node ellipse,#mermaid-svg-1okPqkMxkgV0qmk6 .node polygon,#mermaid-svg-1okPqkMxkgV0qmk6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1okPqkMxkgV0qmk6 .rough-node .label text,#mermaid-svg-1okPqkMxkgV0qmk6 .node .label text,#mermaid-svg-1okPqkMxkgV0qmk6 .image-shape .label,#mermaid-svg-1okPqkMxkgV0qmk6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-1okPqkMxkgV0qmk6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1okPqkMxkgV0qmk6 .rough-node .label,#mermaid-svg-1okPqkMxkgV0qmk6 .node .label,#mermaid-svg-1okPqkMxkgV0qmk6 .image-shape .label,#mermaid-svg-1okPqkMxkgV0qmk6 .icon-shape .label{text-align:center;}#mermaid-svg-1okPqkMxkgV0qmk6 .node.clickable{cursor:pointer;}#mermaid-svg-1okPqkMxkgV0qmk6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1okPqkMxkgV0qmk6 .arrowheadPath{fill:#333333;}#mermaid-svg-1okPqkMxkgV0qmk6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1okPqkMxkgV0qmk6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1okPqkMxkgV0qmk6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1okPqkMxkgV0qmk6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1okPqkMxkgV0qmk6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1okPqkMxkgV0qmk6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1okPqkMxkgV0qmk6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1okPqkMxkgV0qmk6 .cluster text{fill:#333;}#mermaid-svg-1okPqkMxkgV0qmk6 .cluster span{color:#333;}#mermaid-svg-1okPqkMxkgV0qmk6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1okPqkMxkgV0qmk6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1okPqkMxkgV0qmk6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-1okPqkMxkgV0qmk6 .icon-shape,#mermaid-svg-1okPqkMxkgV0qmk6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1okPqkMxkgV0qmk6 .icon-shape p,#mermaid-svg-1okPqkMxkgV0qmk6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1okPqkMxkgV0qmk6 .icon-shape .label rect,#mermaid-svg-1okPqkMxkgV0qmk6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1okPqkMxkgV0qmk6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1okPqkMxkgV0qmk6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1okPqkMxkgV0qmk6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AIR原则
Automatic 自动化
Independent 独立性
Repeatable 可重复
无需人工干预
可集成CI/CD
用例之间独立
不依赖执行顺序
任何环境都通过
不依赖时间和随机数
AIR原则详解:
| 原则 | 英文 | 说明 | 违反示例 |
|---|---|---|---|
| A | Automatic | 测试应完全自动化执行 | 需要手动配置环境才能运行 |
| I | Independent | 测试用例之间相互独立 | 用例B依赖用例A创建的数据 |
| R | Repeatable | 在任何环境下都能重复执行 | 依赖本地时间或网络环境 |
(二)BCDE 原则
BCDE原则提供了更具体的设计指导:
| 原则 | 英文 | 说明 | 实践方法 |
|---|---|---|---|
| B | Border | 边界值测试 | 测试输入的边界条件 |
| C | Correct | 正确的输入 | 测试正常输入能否得到正确结果 |
| D | Design | 与设计文档结合 | 根据设计文档编写测试 |
| E | Error | 错误的输入 | 测试非法输入能否正确处理 |
BCDE实践示例:
#mermaid-svg-82JIqoX7tb3FP23g{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-82JIqoX7tb3FP23g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-82JIqoX7tb3FP23g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-82JIqoX7tb3FP23g .error-icon{fill:#552222;}#mermaid-svg-82JIqoX7tb3FP23g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-82JIqoX7tb3FP23g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-82JIqoX7tb3FP23g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-82JIqoX7tb3FP23g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-82JIqoX7tb3FP23g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-82JIqoX7tb3FP23g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-82JIqoX7tb3FP23g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-82JIqoX7tb3FP23g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-82JIqoX7tb3FP23g .marker.cross{stroke:#333333;}#mermaid-svg-82JIqoX7tb3FP23g svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-82JIqoX7tb3FP23g p{margin:0;}#mermaid-svg-82JIqoX7tb3FP23g .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-82JIqoX7tb3FP23g .cluster-label text{fill:#333;}#mermaid-svg-82JIqoX7tb3FP23g .cluster-label span{color:#333;}#mermaid-svg-82JIqoX7tb3FP23g .cluster-label span p{background-color:transparent;}#mermaid-svg-82JIqoX7tb3FP23g .label text,#mermaid-svg-82JIqoX7tb3FP23g span{fill:#333;color:#333;}#mermaid-svg-82JIqoX7tb3FP23g .node rect,#mermaid-svg-82JIqoX7tb3FP23g .node circle,#mermaid-svg-82JIqoX7tb3FP23g .node ellipse,#mermaid-svg-82JIqoX7tb3FP23g .node polygon,#mermaid-svg-82JIqoX7tb3FP23g .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-82JIqoX7tb3FP23g .rough-node .label text,#mermaid-svg-82JIqoX7tb3FP23g .node .label text,#mermaid-svg-82JIqoX7tb3FP23g .image-shape .label,#mermaid-svg-82JIqoX7tb3FP23g .icon-shape .label{text-anchor:middle;}#mermaid-svg-82JIqoX7tb3FP23g .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-82JIqoX7tb3FP23g .rough-node .label,#mermaid-svg-82JIqoX7tb3FP23g .node .label,#mermaid-svg-82JIqoX7tb3FP23g .image-shape .label,#mermaid-svg-82JIqoX7tb3FP23g .icon-shape .label{text-align:center;}#mermaid-svg-82JIqoX7tb3FP23g .node.clickable{cursor:pointer;}#mermaid-svg-82JIqoX7tb3FP23g .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-82JIqoX7tb3FP23g .arrowheadPath{fill:#333333;}#mermaid-svg-82JIqoX7tb3FP23g .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-82JIqoX7tb3FP23g .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-82JIqoX7tb3FP23g .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-82JIqoX7tb3FP23g .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-82JIqoX7tb3FP23g .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-82JIqoX7tb3FP23g .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-82JIqoX7tb3FP23g .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-82JIqoX7tb3FP23g .cluster text{fill:#333;}#mermaid-svg-82JIqoX7tb3FP23g .cluster span{color:#333;}#mermaid-svg-82JIqoX7tb3FP23g div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-82JIqoX7tb3FP23g .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-82JIqoX7tb3FP23g rect.text{fill:none;stroke-width:0;}#mermaid-svg-82JIqoX7tb3FP23g .icon-shape,#mermaid-svg-82JIqoX7tb3FP23g .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-82JIqoX7tb3FP23g .icon-shape p,#mermaid-svg-82JIqoX7tb3FP23g .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-82JIqoX7tb3FP23g .icon-shape .label rect,#mermaid-svg-82JIqoX7tb3FP23g .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-82JIqoX7tb3FP23g .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-82JIqoX7tb3FP23g .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-82JIqoX7tb3FP23g :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} BCDE原则实践
B-边界值
C-正确输入
D-设计对齐
E-错误输入
空值/最大值/最小值
典型合法输入
对照需求文档
非法类型/越界值
(三)FIRM 原则
FIRM原则关注测试代码本身的质量:
| 原则 | 英文 | 说明 |
|---|---|---|
| F | Fast | 测试要快,快速反馈 |
| I | Isolated | 测试隔离,不依赖外部环境 |
| R | Repeatable | 可重复,结果稳定 |
| M | Self-validating | 自验证,自动判断通过或失败 |
💡 关键理解:原则是约束,更是保障。一个违反独立性原则的单元测试,在CI/CD中可能时过时不过,最终被开发者弃用。一个不可重复的测试,会让团队陷入"到底是不是bug"的争论中。好的单元测试就像好的代码------简洁、独立、可维护。
🔧 三、单元测试的方法与技术
(一)静态测试方法
静态测试是不运行代码、通过检查代码本身来发现问题的方法。
静态测试内容:
#mermaid-svg-Sfje1fWZPSJwrqWE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Sfje1fWZPSJwrqWE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Sfje1fWZPSJwrqWE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Sfje1fWZPSJwrqWE .error-icon{fill:#552222;}#mermaid-svg-Sfje1fWZPSJwrqWE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Sfje1fWZPSJwrqWE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Sfje1fWZPSJwrqWE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Sfje1fWZPSJwrqWE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Sfje1fWZPSJwrqWE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Sfje1fWZPSJwrqWE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Sfje1fWZPSJwrqWE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Sfje1fWZPSJwrqWE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Sfje1fWZPSJwrqWE .marker.cross{stroke:#333333;}#mermaid-svg-Sfje1fWZPSJwrqWE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Sfje1fWZPSJwrqWE p{margin:0;}#mermaid-svg-Sfje1fWZPSJwrqWE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Sfje1fWZPSJwrqWE .cluster-label text{fill:#333;}#mermaid-svg-Sfje1fWZPSJwrqWE .cluster-label span{color:#333;}#mermaid-svg-Sfje1fWZPSJwrqWE .cluster-label span p{background-color:transparent;}#mermaid-svg-Sfje1fWZPSJwrqWE .label text,#mermaid-svg-Sfje1fWZPSJwrqWE span{fill:#333;color:#333;}#mermaid-svg-Sfje1fWZPSJwrqWE .node rect,#mermaid-svg-Sfje1fWZPSJwrqWE .node circle,#mermaid-svg-Sfje1fWZPSJwrqWE .node ellipse,#mermaid-svg-Sfje1fWZPSJwrqWE .node polygon,#mermaid-svg-Sfje1fWZPSJwrqWE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Sfje1fWZPSJwrqWE .rough-node .label text,#mermaid-svg-Sfje1fWZPSJwrqWE .node .label text,#mermaid-svg-Sfje1fWZPSJwrqWE .image-shape .label,#mermaid-svg-Sfje1fWZPSJwrqWE .icon-shape .label{text-anchor:middle;}#mermaid-svg-Sfje1fWZPSJwrqWE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Sfje1fWZPSJwrqWE .rough-node .label,#mermaid-svg-Sfje1fWZPSJwrqWE .node .label,#mermaid-svg-Sfje1fWZPSJwrqWE .image-shape .label,#mermaid-svg-Sfje1fWZPSJwrqWE .icon-shape .label{text-align:center;}#mermaid-svg-Sfje1fWZPSJwrqWE .node.clickable{cursor:pointer;}#mermaid-svg-Sfje1fWZPSJwrqWE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Sfje1fWZPSJwrqWE .arrowheadPath{fill:#333333;}#mermaid-svg-Sfje1fWZPSJwrqWE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Sfje1fWZPSJwrqWE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Sfje1fWZPSJwrqWE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sfje1fWZPSJwrqWE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Sfje1fWZPSJwrqWE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sfje1fWZPSJwrqWE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Sfje1fWZPSJwrqWE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Sfje1fWZPSJwrqWE .cluster text{fill:#333;}#mermaid-svg-Sfje1fWZPSJwrqWE .cluster span{color:#333;}#mermaid-svg-Sfje1fWZPSJwrqWE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Sfje1fWZPSJwrqWE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Sfje1fWZPSJwrqWE rect.text{fill:none;stroke-width:0;}#mermaid-svg-Sfje1fWZPSJwrqWE .icon-shape,#mermaid-svg-Sfje1fWZPSJwrqWE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sfje1fWZPSJwrqWE .icon-shape p,#mermaid-svg-Sfje1fWZPSJwrqWE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Sfje1fWZPSJwrqWE .icon-shape .label rect,#mermaid-svg-Sfje1fWZPSJwrqWE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sfje1fWZPSJwrqWE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Sfje1fWZPSJwrqWE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Sfje1fWZPSJwrqWE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 静态测试方法
代码审查
代码走查
静态分析
桌面检查
同行评审
正式审查会
规则检查
代码规范
工具扫描
复杂度分析
开发者自查
逻辑推演
静态测试方法对比:
| 方法 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 代码审查 | 由同事检查代码 | 发现设计缺陷、知识共享 | 耗时、依赖审查者经验 |
| 代码走查 | 开发者主导讲解代码 | 深入理解代码逻辑 | 可能遗漏问题 |
| 静态分析 | 工具自动扫描代码 | 高效、覆盖全面 | 无法发现逻辑错误 |
| 桌面检查 | 开发者人工推演代码 | 简单快速 | 容易遗漏边界情况 |
(二)动态测试方法
动态测试是通过运行代码来验证其行为的方法。
动态测试内容:
#mermaid-svg-P9fczEvvht4QP3J2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-P9fczEvvht4QP3J2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-P9fczEvvht4QP3J2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-P9fczEvvht4QP3J2 .error-icon{fill:#552222;}#mermaid-svg-P9fczEvvht4QP3J2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-P9fczEvvht4QP3J2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-P9fczEvvht4QP3J2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-P9fczEvvht4QP3J2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-P9fczEvvht4QP3J2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-P9fczEvvht4QP3J2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-P9fczEvvht4QP3J2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-P9fczEvvht4QP3J2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-P9fczEvvht4QP3J2 .marker.cross{stroke:#333333;}#mermaid-svg-P9fczEvvht4QP3J2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-P9fczEvvht4QP3J2 p{margin:0;}#mermaid-svg-P9fczEvvht4QP3J2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-P9fczEvvht4QP3J2 .cluster-label text{fill:#333;}#mermaid-svg-P9fczEvvht4QP3J2 .cluster-label span{color:#333;}#mermaid-svg-P9fczEvvht4QP3J2 .cluster-label span p{background-color:transparent;}#mermaid-svg-P9fczEvvht4QP3J2 .label text,#mermaid-svg-P9fczEvvht4QP3J2 span{fill:#333;color:#333;}#mermaid-svg-P9fczEvvht4QP3J2 .node rect,#mermaid-svg-P9fczEvvht4QP3J2 .node circle,#mermaid-svg-P9fczEvvht4QP3J2 .node ellipse,#mermaid-svg-P9fczEvvht4QP3J2 .node polygon,#mermaid-svg-P9fczEvvht4QP3J2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P9fczEvvht4QP3J2 .rough-node .label text,#mermaid-svg-P9fczEvvht4QP3J2 .node .label text,#mermaid-svg-P9fczEvvht4QP3J2 .image-shape .label,#mermaid-svg-P9fczEvvht4QP3J2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-P9fczEvvht4QP3J2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-P9fczEvvht4QP3J2 .rough-node .label,#mermaid-svg-P9fczEvvht4QP3J2 .node .label,#mermaid-svg-P9fczEvvht4QP3J2 .image-shape .label,#mermaid-svg-P9fczEvvht4QP3J2 .icon-shape .label{text-align:center;}#mermaid-svg-P9fczEvvht4QP3J2 .node.clickable{cursor:pointer;}#mermaid-svg-P9fczEvvht4QP3J2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-P9fczEvvht4QP3J2 .arrowheadPath{fill:#333333;}#mermaid-svg-P9fczEvvht4QP3J2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-P9fczEvvht4QP3J2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-P9fczEvvht4QP3J2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P9fczEvvht4QP3J2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-P9fczEvvht4QP3J2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P9fczEvvht4QP3J2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-P9fczEvvht4QP3J2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-P9fczEvvht4QP3J2 .cluster text{fill:#333;}#mermaid-svg-P9fczEvvht4QP3J2 .cluster span{color:#333;}#mermaid-svg-P9fczEvvht4QP3J2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-P9fczEvvht4QP3J2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-P9fczEvvht4QP3J2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-P9fczEvvht4QP3J2 .icon-shape,#mermaid-svg-P9fczEvvht4QP3J2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P9fczEvvht4QP3J2 .icon-shape p,#mermaid-svg-P9fczEvvht4QP3J2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-P9fczEvvht4QP3J2 .icon-shape .label rect,#mermaid-svg-P9fczEvvht4QP3J2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P9fczEvvht4QP3J2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-P9fczEvvht4QP3J2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-P9fczEvvht4QP3J2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 动态测试方法
逻辑覆盖
基本路径测试
等价类划分
边界值分析
语句覆盖
分支覆盖
条件覆盖
路径覆盖
圈复杂度
独立路径
输入分类
边界值选取
1. 逻辑覆盖法
逻辑覆盖是通过对程序逻辑结构的遍历来设计测试用例的方法。
覆盖级别:
| 覆盖类型 | 说明 | 覆盖强度 | 示例 |
|---|---|---|---|
| 语句覆盖 | 每条语句至少执行一次 | 最弱 | 让代码每一行都跑一遍 |
| 判定覆盖 | 每个判定的真假分支各执行一次 | 较弱 | if条件为true和false各测一次 |
| 条件覆盖 | 每个条件的所有可能取值各执行一次 | 中等 | a>0和a<=0各测一次 |
| 判定/条件覆盖 | 同时满足判定覆盖和条件覆盖 | 较强 | 判定和条件都覆盖 |
| 条件组合覆盖 | 所有条件的各种组合都执行一次 | 强 | a>0且b>0、a>0且b<=0等全组合 |
| 路径覆盖 | 程序所有可能的执行路径都执行一次 | 最强 | 从入口到出口的每条路径 |
覆盖强度关系:
#mermaid-svg-fSKK4NAjxiOGcImx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fSKK4NAjxiOGcImx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fSKK4NAjxiOGcImx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fSKK4NAjxiOGcImx .error-icon{fill:#552222;}#mermaid-svg-fSKK4NAjxiOGcImx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fSKK4NAjxiOGcImx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fSKK4NAjxiOGcImx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fSKK4NAjxiOGcImx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fSKK4NAjxiOGcImx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fSKK4NAjxiOGcImx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fSKK4NAjxiOGcImx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fSKK4NAjxiOGcImx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fSKK4NAjxiOGcImx .marker.cross{stroke:#333333;}#mermaid-svg-fSKK4NAjxiOGcImx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fSKK4NAjxiOGcImx p{margin:0;}#mermaid-svg-fSKK4NAjxiOGcImx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fSKK4NAjxiOGcImx .cluster-label text{fill:#333;}#mermaid-svg-fSKK4NAjxiOGcImx .cluster-label span{color:#333;}#mermaid-svg-fSKK4NAjxiOGcImx .cluster-label span p{background-color:transparent;}#mermaid-svg-fSKK4NAjxiOGcImx .label text,#mermaid-svg-fSKK4NAjxiOGcImx span{fill:#333;color:#333;}#mermaid-svg-fSKK4NAjxiOGcImx .node rect,#mermaid-svg-fSKK4NAjxiOGcImx .node circle,#mermaid-svg-fSKK4NAjxiOGcImx .node ellipse,#mermaid-svg-fSKK4NAjxiOGcImx .node polygon,#mermaid-svg-fSKK4NAjxiOGcImx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fSKK4NAjxiOGcImx .rough-node .label text,#mermaid-svg-fSKK4NAjxiOGcImx .node .label text,#mermaid-svg-fSKK4NAjxiOGcImx .image-shape .label,#mermaid-svg-fSKK4NAjxiOGcImx .icon-shape .label{text-anchor:middle;}#mermaid-svg-fSKK4NAjxiOGcImx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fSKK4NAjxiOGcImx .rough-node .label,#mermaid-svg-fSKK4NAjxiOGcImx .node .label,#mermaid-svg-fSKK4NAjxiOGcImx .image-shape .label,#mermaid-svg-fSKK4NAjxiOGcImx .icon-shape .label{text-align:center;}#mermaid-svg-fSKK4NAjxiOGcImx .node.clickable{cursor:pointer;}#mermaid-svg-fSKK4NAjxiOGcImx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fSKK4NAjxiOGcImx .arrowheadPath{fill:#333333;}#mermaid-svg-fSKK4NAjxiOGcImx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fSKK4NAjxiOGcImx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fSKK4NAjxiOGcImx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fSKK4NAjxiOGcImx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fSKK4NAjxiOGcImx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fSKK4NAjxiOGcImx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fSKK4NAjxiOGcImx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fSKK4NAjxiOGcImx .cluster text{fill:#333;}#mermaid-svg-fSKK4NAjxiOGcImx .cluster span{color:#333;}#mermaid-svg-fSKK4NAjxiOGcImx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fSKK4NAjxiOGcImx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fSKK4NAjxiOGcImx rect.text{fill:none;stroke-width:0;}#mermaid-svg-fSKK4NAjxiOGcImx .icon-shape,#mermaid-svg-fSKK4NAjxiOGcImx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fSKK4NAjxiOGcImx .icon-shape p,#mermaid-svg-fSKK4NAjxiOGcImx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fSKK4NAjxiOGcImx .icon-shape .label rect,#mermaid-svg-fSKK4NAjxiOGcImx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fSKK4NAjxiOGcImx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fSKK4NAjxiOGcImx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fSKK4NAjxiOGcImx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 路径覆盖
条件组合覆盖
判定/条件覆盖
条件覆盖
判定覆盖
语句覆盖
💡 关键理解:逻辑覆盖是一个由弱到强的层次体系。语句覆盖是最基本的,但往往不够------它不关心条件是否正确取到了所有值。路径覆盖最强,但路径数可能指数级增长,实践中通常使用判定覆盖或条件组合覆盖作为最低标准。
2. 基本路径测试法
基本路径测试基于程序的圈复杂度来确定一组基本路径,确保每条独立路径至少执行一次。
基本路径测试步骤:
| 步骤 | 任务 | 产出 |
|---|---|---|
| 1. 画控制流图 | 将代码转化为流程图 | 控制流图 |
| 2. 计算圈复杂度 | V(G) = E - N + 2 | 圈复杂度值 |
| 3. 确定基本路径 | 找出所有独立路径 | 基本路径集 |
| 4. 设计测试用例 | 为每条路径设计用例 | 测试用例集 |
圈复杂度计算公式:
| 方法 | 公式 | 说明 |
|---|---|---|
| 边节点法 | V(G) = E - N + 2 | E为边数,N为节点数 |
| 判定节点法 | V(G) = P + 1 | P为判定节点数 |
| 区域法 | V(G) = 区域数 | 控制流图被划分的区域数 |
3. 基于测试框架的方法
现代单元测试通常借助测试框架来组织和执行。
测试框架使用流程:
#mermaid-svg-QTJTeggUuraIsxz1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QTJTeggUuraIsxz1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QTJTeggUuraIsxz1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QTJTeggUuraIsxz1 .error-icon{fill:#552222;}#mermaid-svg-QTJTeggUuraIsxz1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QTJTeggUuraIsxz1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QTJTeggUuraIsxz1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QTJTeggUuraIsxz1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QTJTeggUuraIsxz1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QTJTeggUuraIsxz1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QTJTeggUuraIsxz1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QTJTeggUuraIsxz1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QTJTeggUuraIsxz1 .marker.cross{stroke:#333333;}#mermaid-svg-QTJTeggUuraIsxz1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QTJTeggUuraIsxz1 p{margin:0;}#mermaid-svg-QTJTeggUuraIsxz1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QTJTeggUuraIsxz1 .cluster-label text{fill:#333;}#mermaid-svg-QTJTeggUuraIsxz1 .cluster-label span{color:#333;}#mermaid-svg-QTJTeggUuraIsxz1 .cluster-label span p{background-color:transparent;}#mermaid-svg-QTJTeggUuraIsxz1 .label text,#mermaid-svg-QTJTeggUuraIsxz1 span{fill:#333;color:#333;}#mermaid-svg-QTJTeggUuraIsxz1 .node rect,#mermaid-svg-QTJTeggUuraIsxz1 .node circle,#mermaid-svg-QTJTeggUuraIsxz1 .node ellipse,#mermaid-svg-QTJTeggUuraIsxz1 .node polygon,#mermaid-svg-QTJTeggUuraIsxz1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QTJTeggUuraIsxz1 .rough-node .label text,#mermaid-svg-QTJTeggUuraIsxz1 .node .label text,#mermaid-svg-QTJTeggUuraIsxz1 .image-shape .label,#mermaid-svg-QTJTeggUuraIsxz1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-QTJTeggUuraIsxz1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QTJTeggUuraIsxz1 .rough-node .label,#mermaid-svg-QTJTeggUuraIsxz1 .node .label,#mermaid-svg-QTJTeggUuraIsxz1 .image-shape .label,#mermaid-svg-QTJTeggUuraIsxz1 .icon-shape .label{text-align:center;}#mermaid-svg-QTJTeggUuraIsxz1 .node.clickable{cursor:pointer;}#mermaid-svg-QTJTeggUuraIsxz1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QTJTeggUuraIsxz1 .arrowheadPath{fill:#333333;}#mermaid-svg-QTJTeggUuraIsxz1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QTJTeggUuraIsxz1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QTJTeggUuraIsxz1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QTJTeggUuraIsxz1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QTJTeggUuraIsxz1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QTJTeggUuraIsxz1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QTJTeggUuraIsxz1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QTJTeggUuraIsxz1 .cluster text{fill:#333;}#mermaid-svg-QTJTeggUuraIsxz1 .cluster span{color:#333;}#mermaid-svg-QTJTeggUuraIsxz1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QTJTeggUuraIsxz1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QTJTeggUuraIsxz1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-QTJTeggUuraIsxz1 .icon-shape,#mermaid-svg-QTJTeggUuraIsxz1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QTJTeggUuraIsxz1 .icon-shape p,#mermaid-svg-QTJTeggUuraIsxz1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QTJTeggUuraIsxz1 .icon-shape .label rect,#mermaid-svg-QTJTeggUuraIsxz1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QTJTeggUuraIsxz1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QTJTeggUuraIsxz1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QTJTeggUuraIsxz1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
编写被测代码
编写测试用例
运行测试框架
查看测试报告
是否全部通过?
代码提交
定位失败用例
修复代码
主流测试框架:
| 语言/平台 | 测试框架 | 特点 |
|---|---|---|
| Java | JUnit | 最经典的Java测试框架,注解驱动 |
| Python | pytest / unittest | pytest语法简洁,unittest内置标准库 |
| JavaScript | Jest / Mocha | Jest零配置,Mocha灵活扩展 |
| C# | NUnit / MSTest | NUnit功能丰富,MSTest集成VS |
| Go | testing包 | 内置测试框架,简洁高效 |
| PHP | PHPUnit | PHP标准测试框架 |
🔄 四、TDD 测试驱动开发
(一)TDD 概述
测试驱动开发(Test-Driven Development)是一种先写测试、再写代码的开发方法论。
TDD 内容:
#mermaid-svg-ONrMATWJGqKJdJvT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ONrMATWJGqKJdJvT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ONrMATWJGqKJdJvT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ONrMATWJGqKJdJvT .error-icon{fill:#552222;}#mermaid-svg-ONrMATWJGqKJdJvT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ONrMATWJGqKJdJvT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ONrMATWJGqKJdJvT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ONrMATWJGqKJdJvT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ONrMATWJGqKJdJvT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ONrMATWJGqKJdJvT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ONrMATWJGqKJdJvT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ONrMATWJGqKJdJvT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ONrMATWJGqKJdJvT .marker.cross{stroke:#333333;}#mermaid-svg-ONrMATWJGqKJdJvT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ONrMATWJGqKJdJvT p{margin:0;}#mermaid-svg-ONrMATWJGqKJdJvT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ONrMATWJGqKJdJvT .cluster-label text{fill:#333;}#mermaid-svg-ONrMATWJGqKJdJvT .cluster-label span{color:#333;}#mermaid-svg-ONrMATWJGqKJdJvT .cluster-label span p{background-color:transparent;}#mermaid-svg-ONrMATWJGqKJdJvT .label text,#mermaid-svg-ONrMATWJGqKJdJvT span{fill:#333;color:#333;}#mermaid-svg-ONrMATWJGqKJdJvT .node rect,#mermaid-svg-ONrMATWJGqKJdJvT .node circle,#mermaid-svg-ONrMATWJGqKJdJvT .node ellipse,#mermaid-svg-ONrMATWJGqKJdJvT .node polygon,#mermaid-svg-ONrMATWJGqKJdJvT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ONrMATWJGqKJdJvT .rough-node .label text,#mermaid-svg-ONrMATWJGqKJdJvT .node .label text,#mermaid-svg-ONrMATWJGqKJdJvT .image-shape .label,#mermaid-svg-ONrMATWJGqKJdJvT .icon-shape .label{text-anchor:middle;}#mermaid-svg-ONrMATWJGqKJdJvT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ONrMATWJGqKJdJvT .rough-node .label,#mermaid-svg-ONrMATWJGqKJdJvT .node .label,#mermaid-svg-ONrMATWJGqKJdJvT .image-shape .label,#mermaid-svg-ONrMATWJGqKJdJvT .icon-shape .label{text-align:center;}#mermaid-svg-ONrMATWJGqKJdJvT .node.clickable{cursor:pointer;}#mermaid-svg-ONrMATWJGqKJdJvT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ONrMATWJGqKJdJvT .arrowheadPath{fill:#333333;}#mermaid-svg-ONrMATWJGqKJdJvT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ONrMATWJGqKJdJvT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ONrMATWJGqKJdJvT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ONrMATWJGqKJdJvT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ONrMATWJGqKJdJvT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ONrMATWJGqKJdJvT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ONrMATWJGqKJdJvT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ONrMATWJGqKJdJvT .cluster text{fill:#333;}#mermaid-svg-ONrMATWJGqKJdJvT .cluster span{color:#333;}#mermaid-svg-ONrMATWJGqKJdJvT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ONrMATWJGqKJdJvT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ONrMATWJGqKJdJvT rect.text{fill:none;stroke-width:0;}#mermaid-svg-ONrMATWJGqKJdJvT .icon-shape,#mermaid-svg-ONrMATWJGqKJdJvT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ONrMATWJGqKJdJvT .icon-shape p,#mermaid-svg-ONrMATWJGqKJdJvT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ONrMATWJGqKJdJvT .icon-shape .label rect,#mermaid-svg-ONrMATWJGqKJdJvT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ONrMATWJGqKJdJvT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ONrMATWJGqKJdJvT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ONrMATWJGqKJdJvT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TDD循环
Red-写失败测试
Green-写最少代码通过测试
Refactor-重构优化代码
TDD 三步骤:
| 步骤 | 名称 | 任务 | 状态 |
|---|---|---|---|
| 1 | Red(红灯) | 编写一个会失败的测试用例 | 测试失败 |
| 2 | Green(绿灯) | 编写最少的代码让测试通过 | 测试通过 |
| 3 | Refactor(重构) | 优化代码结构,保持测试通过 | 测试通过 |
(二)TDD 的优势与挑战
优势与挑战:
| 维度 | 优势 | 挑战 |
|---|---|---|
| 设计质量 | 测试先行倒逼良好设计 | 需要设计思维,前期投入大 |
| 代码质量 | 高测试覆盖率,低缺陷率 | 团队需要适应新的开发模式 |
| 文档价值 | 测试即文档,接口清晰 | 编写测试增加开发时间 |
| 信心保障 | 重构时有安全网 | 需要纪律性,不能跳过步骤 |
| 需求理解 | 促使深入理解需求 | 需求不明确时难以先写测试 |
(三)TDD 与传统开发对比
开发流程对比:
#mermaid-svg-NUkHcmgy3FMYLlOO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NUkHcmgy3FMYLlOO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NUkHcmgy3FMYLlOO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NUkHcmgy3FMYLlOO .error-icon{fill:#552222;}#mermaid-svg-NUkHcmgy3FMYLlOO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NUkHcmgy3FMYLlOO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NUkHcmgy3FMYLlOO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NUkHcmgy3FMYLlOO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NUkHcmgy3FMYLlOO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NUkHcmgy3FMYLlOO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NUkHcmgy3FMYLlOO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NUkHcmgy3FMYLlOO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NUkHcmgy3FMYLlOO .marker.cross{stroke:#333333;}#mermaid-svg-NUkHcmgy3FMYLlOO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NUkHcmgy3FMYLlOO p{margin:0;}#mermaid-svg-NUkHcmgy3FMYLlOO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NUkHcmgy3FMYLlOO .cluster-label text{fill:#333;}#mermaid-svg-NUkHcmgy3FMYLlOO .cluster-label span{color:#333;}#mermaid-svg-NUkHcmgy3FMYLlOO .cluster-label span p{background-color:transparent;}#mermaid-svg-NUkHcmgy3FMYLlOO .label text,#mermaid-svg-NUkHcmgy3FMYLlOO span{fill:#333;color:#333;}#mermaid-svg-NUkHcmgy3FMYLlOO .node rect,#mermaid-svg-NUkHcmgy3FMYLlOO .node circle,#mermaid-svg-NUkHcmgy3FMYLlOO .node ellipse,#mermaid-svg-NUkHcmgy3FMYLlOO .node polygon,#mermaid-svg-NUkHcmgy3FMYLlOO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NUkHcmgy3FMYLlOO .rough-node .label text,#mermaid-svg-NUkHcmgy3FMYLlOO .node .label text,#mermaid-svg-NUkHcmgy3FMYLlOO .image-shape .label,#mermaid-svg-NUkHcmgy3FMYLlOO .icon-shape .label{text-anchor:middle;}#mermaid-svg-NUkHcmgy3FMYLlOO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NUkHcmgy3FMYLlOO .rough-node .label,#mermaid-svg-NUkHcmgy3FMYLlOO .node .label,#mermaid-svg-NUkHcmgy3FMYLlOO .image-shape .label,#mermaid-svg-NUkHcmgy3FMYLlOO .icon-shape .label{text-align:center;}#mermaid-svg-NUkHcmgy3FMYLlOO .node.clickable{cursor:pointer;}#mermaid-svg-NUkHcmgy3FMYLlOO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NUkHcmgy3FMYLlOO .arrowheadPath{fill:#333333;}#mermaid-svg-NUkHcmgy3FMYLlOO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NUkHcmgy3FMYLlOO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NUkHcmgy3FMYLlOO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NUkHcmgy3FMYLlOO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NUkHcmgy3FMYLlOO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NUkHcmgy3FMYLlOO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NUkHcmgy3FMYLlOO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NUkHcmgy3FMYLlOO .cluster text{fill:#333;}#mermaid-svg-NUkHcmgy3FMYLlOO .cluster span{color:#333;}#mermaid-svg-NUkHcmgy3FMYLlOO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NUkHcmgy3FMYLlOO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NUkHcmgy3FMYLlOO rect.text{fill:none;stroke-width:0;}#mermaid-svg-NUkHcmgy3FMYLlOO .icon-shape,#mermaid-svg-NUkHcmgy3FMYLlOO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NUkHcmgy3FMYLlOO .icon-shape p,#mermaid-svg-NUkHcmgy3FMYLlOO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NUkHcmgy3FMYLlOO .icon-shape .label rect,#mermaid-svg-NUkHcmgy3FMYLlOO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NUkHcmgy3FMYLlOO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NUkHcmgy3FMYLlOO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NUkHcmgy3FMYLlOO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 传统开发
编码
测试
修复
TDD开发
写测试
编码
重构
| 对比维度 | 传统开发 | TDD开发 |
|---|---|---|
| 开发顺序 | 先编码后测试 | 先测试后编码 |
| 测试覆盖率 | 通常较低 | 接近100% |
| 缺陷发现时机 | 测试阶段 | 编码阶段 |
| 设计驱动 | 开发经验驱动 | 测试用例驱动 |
| 重构信心 | 可能引入新bug | 测试保障,安全重构 |
| 开发效率 | 初期快,后期维护成本高 | 初期慢,长期维护成本低 |
💡 关键理解:TDD的本质不是"先写测试"这个动作,而是一种"先想清楚再动手"的思维方式。它迫使开发者在写代码之前,先思考"这段代码应该做什么",从而写出更清晰、更可测试的代码。TDD不是银弹,但在需求相对明确的场景下,它能显著提升代码质量和开发效率。
📊 五、单元测试的 Mock 与 Stub
(一)为什么需要 Mock
隔离原则要求单元测试不依赖外部资源,Mock就是为了实现隔离。
Mock 内容:
#mermaid-svg-4xnS1y8Wz1RfVWv4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .error-icon{fill:#552222;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .marker.cross{stroke:#333333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4xnS1y8Wz1RfVWv4 p{margin:0;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .cluster-label text{fill:#333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .cluster-label span{color:#333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .cluster-label span p{background-color:transparent;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .label text,#mermaid-svg-4xnS1y8Wz1RfVWv4 span{fill:#333;color:#333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .node rect,#mermaid-svg-4xnS1y8Wz1RfVWv4 .node circle,#mermaid-svg-4xnS1y8Wz1RfVWv4 .node ellipse,#mermaid-svg-4xnS1y8Wz1RfVWv4 .node polygon,#mermaid-svg-4xnS1y8Wz1RfVWv4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .rough-node .label text,#mermaid-svg-4xnS1y8Wz1RfVWv4 .node .label text,#mermaid-svg-4xnS1y8Wz1RfVWv4 .image-shape .label,#mermaid-svg-4xnS1y8Wz1RfVWv4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .rough-node .label,#mermaid-svg-4xnS1y8Wz1RfVWv4 .node .label,#mermaid-svg-4xnS1y8Wz1RfVWv4 .image-shape .label,#mermaid-svg-4xnS1y8Wz1RfVWv4 .icon-shape .label{text-align:center;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .node.clickable{cursor:pointer;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .arrowheadPath{fill:#333333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4xnS1y8Wz1RfVWv4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4xnS1y8Wz1RfVWv4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4xnS1y8Wz1RfVWv4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .cluster text{fill:#333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .cluster span{color:#333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4xnS1y8Wz1RfVWv4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .icon-shape,#mermaid-svg-4xnS1y8Wz1RfVWv4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .icon-shape p,#mermaid-svg-4xnS1y8Wz1RfVWv4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .icon-shape .label rect,#mermaid-svg-4xnS1y8Wz1RfVWv4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4xnS1y8Wz1RfVWv4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4xnS1y8Wz1RfVWv4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4xnS1y8Wz1RfVWv4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 需要Mock的场景
数据库访问
网络请求
文件系统
第三方服务
用Mock替代DB
模拟HTTP响应
模拟文件读写
模拟API返回
(二)Test Double 分类
测试替身(Test Double)是对真实依赖对象的替代,有多种形式。
测试替身分类:
| 类型 | 英文 | 说明 | 使用场景 |
|---|---|---|---|
| 虚拟对象 | Dummy | 占位对象,不会被实际调用 | 方法参数填充 |
| 存根 | Stub | 返回预设值的简单替代 | 提供固定的测试数据 |
| 间谍 | Spy | 记录调用信息的替代 | 验证方法是否被调用 |
| 模拟对象 | Mock | 预设期望并验证调用 | 验证交互行为 |
| 伪装对象 | Fake | 简化实现的真实替代 | 内存数据库替代真实数据库 |
Test Double 对比:
#mermaid-svg-dJyfcrAbyHXFLJoV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dJyfcrAbyHXFLJoV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dJyfcrAbyHXFLJoV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dJyfcrAbyHXFLJoV .error-icon{fill:#552222;}#mermaid-svg-dJyfcrAbyHXFLJoV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dJyfcrAbyHXFLJoV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dJyfcrAbyHXFLJoV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dJyfcrAbyHXFLJoV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dJyfcrAbyHXFLJoV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dJyfcrAbyHXFLJoV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dJyfcrAbyHXFLJoV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dJyfcrAbyHXFLJoV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dJyfcrAbyHXFLJoV .marker.cross{stroke:#333333;}#mermaid-svg-dJyfcrAbyHXFLJoV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dJyfcrAbyHXFLJoV p{margin:0;}#mermaid-svg-dJyfcrAbyHXFLJoV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dJyfcrAbyHXFLJoV .cluster-label text{fill:#333;}#mermaid-svg-dJyfcrAbyHXFLJoV .cluster-label span{color:#333;}#mermaid-svg-dJyfcrAbyHXFLJoV .cluster-label span p{background-color:transparent;}#mermaid-svg-dJyfcrAbyHXFLJoV .label text,#mermaid-svg-dJyfcrAbyHXFLJoV span{fill:#333;color:#333;}#mermaid-svg-dJyfcrAbyHXFLJoV .node rect,#mermaid-svg-dJyfcrAbyHXFLJoV .node circle,#mermaid-svg-dJyfcrAbyHXFLJoV .node ellipse,#mermaid-svg-dJyfcrAbyHXFLJoV .node polygon,#mermaid-svg-dJyfcrAbyHXFLJoV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dJyfcrAbyHXFLJoV .rough-node .label text,#mermaid-svg-dJyfcrAbyHXFLJoV .node .label text,#mermaid-svg-dJyfcrAbyHXFLJoV .image-shape .label,#mermaid-svg-dJyfcrAbyHXFLJoV .icon-shape .label{text-anchor:middle;}#mermaid-svg-dJyfcrAbyHXFLJoV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dJyfcrAbyHXFLJoV .rough-node .label,#mermaid-svg-dJyfcrAbyHXFLJoV .node .label,#mermaid-svg-dJyfcrAbyHXFLJoV .image-shape .label,#mermaid-svg-dJyfcrAbyHXFLJoV .icon-shape .label{text-align:center;}#mermaid-svg-dJyfcrAbyHXFLJoV .node.clickable{cursor:pointer;}#mermaid-svg-dJyfcrAbyHXFLJoV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dJyfcrAbyHXFLJoV .arrowheadPath{fill:#333333;}#mermaid-svg-dJyfcrAbyHXFLJoV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dJyfcrAbyHXFLJoV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dJyfcrAbyHXFLJoV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dJyfcrAbyHXFLJoV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dJyfcrAbyHXFLJoV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dJyfcrAbyHXFLJoV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dJyfcrAbyHXFLJoV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dJyfcrAbyHXFLJoV .cluster text{fill:#333;}#mermaid-svg-dJyfcrAbyHXFLJoV .cluster span{color:#333;}#mermaid-svg-dJyfcrAbyHXFLJoV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dJyfcrAbyHXFLJoV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dJyfcrAbyHXFLJoV rect.text{fill:none;stroke-width:0;}#mermaid-svg-dJyfcrAbyHXFLJoV .icon-shape,#mermaid-svg-dJyfcrAbyHXFLJoV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dJyfcrAbyHXFLJoV .icon-shape p,#mermaid-svg-dJyfcrAbyHXFLJoV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dJyfcrAbyHXFLJoV .icon-shape .label rect,#mermaid-svg-dJyfcrAbyHXFLJoV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dJyfcrAbyHXFLJoV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dJyfcrAbyHXFLJoV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dJyfcrAbyHXFLJoV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Test Double
Dummy 虚拟对象
Stub 存根
Spy 间谍
Mock 模拟对象
Fake 伪装对象
占位不验证
返回固定值
记录调用信息
验证调用行为
简化实现
(三)Mock 使用示例
Mock 使用原则:
| 原则 | 说明 | 示例 |
|---|---|---|
| 只Mock外部依赖 | 不Mock被测对象自身 | Mock数据库连接,不Mock被测方法 |
| 不要过度Mock | 过度Mock导致测试脆弱 | 只Mock不稳定的外部依赖 |
| 验证行为而非实现 | 关注"做了什么"而非"怎么做" | 验证调用了save(),不关心内部逻辑 |
| Mock返回真实场景 | 模拟真实可能的返回值 | 包含正常值和异常值 |
主流Mock框架:
| 语言 | Mock框架 | 特点 |
|---|---|---|
| Java | Mockito | 最流行的Java Mock框架 |
| Python | unittest.mock | 内置Mock库,功能完善 |
| JavaScript | Jest Mock / Sinon | 集成度高或功能灵活 |
| C# | Moq / NSubstitute | 语法简洁,LINQ风格 |
📈 六、单元测试的覆盖率
(一)覆盖率指标
覆盖率是衡量测试充分性的重要指标。
覆盖率内容:
#mermaid-svg-6pqB3UuQoHa0HxOB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6pqB3UuQoHa0HxOB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6pqB3UuQoHa0HxOB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6pqB3UuQoHa0HxOB .error-icon{fill:#552222;}#mermaid-svg-6pqB3UuQoHa0HxOB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6pqB3UuQoHa0HxOB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6pqB3UuQoHa0HxOB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6pqB3UuQoHa0HxOB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6pqB3UuQoHa0HxOB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6pqB3UuQoHa0HxOB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6pqB3UuQoHa0HxOB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6pqB3UuQoHa0HxOB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6pqB3UuQoHa0HxOB .marker.cross{stroke:#333333;}#mermaid-svg-6pqB3UuQoHa0HxOB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6pqB3UuQoHa0HxOB p{margin:0;}#mermaid-svg-6pqB3UuQoHa0HxOB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6pqB3UuQoHa0HxOB .cluster-label text{fill:#333;}#mermaid-svg-6pqB3UuQoHa0HxOB .cluster-label span{color:#333;}#mermaid-svg-6pqB3UuQoHa0HxOB .cluster-label span p{background-color:transparent;}#mermaid-svg-6pqB3UuQoHa0HxOB .label text,#mermaid-svg-6pqB3UuQoHa0HxOB span{fill:#333;color:#333;}#mermaid-svg-6pqB3UuQoHa0HxOB .node rect,#mermaid-svg-6pqB3UuQoHa0HxOB .node circle,#mermaid-svg-6pqB3UuQoHa0HxOB .node ellipse,#mermaid-svg-6pqB3UuQoHa0HxOB .node polygon,#mermaid-svg-6pqB3UuQoHa0HxOB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6pqB3UuQoHa0HxOB .rough-node .label text,#mermaid-svg-6pqB3UuQoHa0HxOB .node .label text,#mermaid-svg-6pqB3UuQoHa0HxOB .image-shape .label,#mermaid-svg-6pqB3UuQoHa0HxOB .icon-shape .label{text-anchor:middle;}#mermaid-svg-6pqB3UuQoHa0HxOB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6pqB3UuQoHa0HxOB .rough-node .label,#mermaid-svg-6pqB3UuQoHa0HxOB .node .label,#mermaid-svg-6pqB3UuQoHa0HxOB .image-shape .label,#mermaid-svg-6pqB3UuQoHa0HxOB .icon-shape .label{text-align:center;}#mermaid-svg-6pqB3UuQoHa0HxOB .node.clickable{cursor:pointer;}#mermaid-svg-6pqB3UuQoHa0HxOB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6pqB3UuQoHa0HxOB .arrowheadPath{fill:#333333;}#mermaid-svg-6pqB3UuQoHa0HxOB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6pqB3UuQoHa0HxOB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6pqB3UuQoHa0HxOB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6pqB3UuQoHa0HxOB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6pqB3UuQoHa0HxOB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6pqB3UuQoHa0HxOB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6pqB3UuQoHa0HxOB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6pqB3UuQoHa0HxOB .cluster text{fill:#333;}#mermaid-svg-6pqB3UuQoHa0HxOB .cluster span{color:#333;}#mermaid-svg-6pqB3UuQoHa0HxOB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6pqB3UuQoHa0HxOB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6pqB3UuQoHa0HxOB rect.text{fill:none;stroke-width:0;}#mermaid-svg-6pqB3UuQoHa0HxOB .icon-shape,#mermaid-svg-6pqB3UuQoHa0HxOB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6pqB3UuQoHa0HxOB .icon-shape p,#mermaid-svg-6pqB3UuQoHa0HxOB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6pqB3UuQoHa0HxOB .icon-shape .label rect,#mermaid-svg-6pqB3UuQoHa0HxOB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6pqB3UuQoHa0HxOB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6pqB3UuQoHa0HxOB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6pqB3UuQoHa0HxOB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 覆盖率指标
语句覆盖率
分支覆盖率
条件覆盖率
行覆盖率
已执行语句/总语句
已覆盖分支/总分值支
已覆盖条件/总条件
已执行行/总代码行
覆盖率指标详解:
| 指标 | 公式 | 说明 | 建议目标 |
|---|---|---|---|
| 语句覆盖率 | 已执行语句数 / 总语句数 × 100% | 最基本的覆盖率指标 | ≥ 80% |
| 分支覆盖率 | 已覆盖分支数 / 总分支数 × 100% | 衡量分支逻辑的覆盖 | ≥ 70% |
| 条件覆盖率 | 已覆盖条件数 / 总条件数 × 100% | 衡量条件表达式的覆盖 | ≥ 70% |
| 行覆盖率 | 已执行代码行数 / 总代码行数 × 100% | 最常用的工程指标 | ≥ 80% |
| 方法覆盖率 | 已调用方法数 / 总方法数 × 100% | 衡量方法的调用覆盖 | ≥ 90% |
(二)覆盖率的误区
覆盖率不等于质量:
| 误区 | 说明 | 正确做法 |
|---|---|---|
| 100%覆盖=没有bug | 覆盖率只说明代码被执行过,不代表断言正确 | 关注断言质量,而非单纯追求数字 |
| 追求覆盖率数字 | 为了覆盖率写无意义的测试 | 以发现缺陷为目标编写测试 |
| 忽略分支条件 | 只关注语句覆盖 | 关注分支和条件覆盖 |
| 一刀切标准 | 所有模块统一覆盖率要求 | 核心模块更高,工具类可适当降低 |
💡 关键理解:覆盖率是一个"下限指标"而非"上限目标"。80%的覆盖率意味着至少80%的代码被测试触碰过,但不意味着这80%没有问题。真正重要的是测试用例的质量------它是否验证了关键逻辑?是否包含了边界和异常场景?一个断言充分的50%覆盖率,比一个断言敷衍的100%覆盖率更有价值。
📝 七、单元测试的最佳实践与注意事项
(一)常见问题
常见问题:
| 问题 | 说明 | 解决方法 |
|---|---|---|
| 测试代码难以维护 | 测试逻辑复杂,修改成本高 | 保持测试简洁,遵循AAA模式 |
| 测试运行慢 | 大量测试执行时间过长 | 合理使用Mock,减少外部依赖 |
| 测试不稳定 | 时过时不过,难以信任 | 消除时间/随机数等不确定因素 |
| 过度测试 | 测试内部实现细节 | 测试行为而非实现 |
| 测试滞后 | 开发完成很久才补测试 | 采用TDD或与编码同步编写 |
| 覆盖率虚高 | 数字好看但质量低 | 关注断言有效性,做Code Review |
(二)AAA 模式
AAA模式是编写单个测试用例的标准结构:
AAA 模式内容:
#mermaid-svg-AtSppXjk32Ltg5py{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AtSppXjk32Ltg5py .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AtSppXjk32Ltg5py .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AtSppXjk32Ltg5py .error-icon{fill:#552222;}#mermaid-svg-AtSppXjk32Ltg5py .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AtSppXjk32Ltg5py .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AtSppXjk32Ltg5py .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AtSppXjk32Ltg5py .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AtSppXjk32Ltg5py .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AtSppXjk32Ltg5py .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AtSppXjk32Ltg5py .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AtSppXjk32Ltg5py .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AtSppXjk32Ltg5py .marker.cross{stroke:#333333;}#mermaid-svg-AtSppXjk32Ltg5py svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AtSppXjk32Ltg5py p{margin:0;}#mermaid-svg-AtSppXjk32Ltg5py .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AtSppXjk32Ltg5py .cluster-label text{fill:#333;}#mermaid-svg-AtSppXjk32Ltg5py .cluster-label span{color:#333;}#mermaid-svg-AtSppXjk32Ltg5py .cluster-label span p{background-color:transparent;}#mermaid-svg-AtSppXjk32Ltg5py .label text,#mermaid-svg-AtSppXjk32Ltg5py span{fill:#333;color:#333;}#mermaid-svg-AtSppXjk32Ltg5py .node rect,#mermaid-svg-AtSppXjk32Ltg5py .node circle,#mermaid-svg-AtSppXjk32Ltg5py .node ellipse,#mermaid-svg-AtSppXjk32Ltg5py .node polygon,#mermaid-svg-AtSppXjk32Ltg5py .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AtSppXjk32Ltg5py .rough-node .label text,#mermaid-svg-AtSppXjk32Ltg5py .node .label text,#mermaid-svg-AtSppXjk32Ltg5py .image-shape .label,#mermaid-svg-AtSppXjk32Ltg5py .icon-shape .label{text-anchor:middle;}#mermaid-svg-AtSppXjk32Ltg5py .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AtSppXjk32Ltg5py .rough-node .label,#mermaid-svg-AtSppXjk32Ltg5py .node .label,#mermaid-svg-AtSppXjk32Ltg5py .image-shape .label,#mermaid-svg-AtSppXjk32Ltg5py .icon-shape .label{text-align:center;}#mermaid-svg-AtSppXjk32Ltg5py .node.clickable{cursor:pointer;}#mermaid-svg-AtSppXjk32Ltg5py .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AtSppXjk32Ltg5py .arrowheadPath{fill:#333333;}#mermaid-svg-AtSppXjk32Ltg5py .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AtSppXjk32Ltg5py .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AtSppXjk32Ltg5py .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AtSppXjk32Ltg5py .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AtSppXjk32Ltg5py .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AtSppXjk32Ltg5py .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AtSppXjk32Ltg5py .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AtSppXjk32Ltg5py .cluster text{fill:#333;}#mermaid-svg-AtSppXjk32Ltg5py .cluster span{color:#333;}#mermaid-svg-AtSppXjk32Ltg5py div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AtSppXjk32Ltg5py .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AtSppXjk32Ltg5py rect.text{fill:none;stroke-width:0;}#mermaid-svg-AtSppXjk32Ltg5py .icon-shape,#mermaid-svg-AtSppXjk32Ltg5py .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AtSppXjk32Ltg5py .icon-shape p,#mermaid-svg-AtSppXjk32Ltg5py .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AtSppXjk32Ltg5py .icon-shape .label rect,#mermaid-svg-AtSppXjk32Ltg5py .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AtSppXjk32Ltg5py .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AtSppXjk32Ltg5py .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AtSppXjk32Ltg5py :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AAA模式
Arrange-准备
Act-执行
Assert-断言
初始化对象/数据
设置前置条件
调用被测方法
验证结果是否符合预期
验证交互行为
AAA模式详解:
| 阶段 | 英文 | 任务 | 代码示例 |
|---|---|---|---|
| Arrange | 准备 | 创建测试数据,设置前置条件 | int a = 5; int b = 3; |
| Act | 执行 | 调用被测方法 | int result = calculator.add(a, b); |
| Assert | 断言 | 验证结果是否符合预期 | assertEquals(8, result); |
(三)最佳实践
最佳实践:
#mermaid-svg-krPrFdX3UsGy96VO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-krPrFdX3UsGy96VO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-krPrFdX3UsGy96VO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-krPrFdX3UsGy96VO .error-icon{fill:#552222;}#mermaid-svg-krPrFdX3UsGy96VO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-krPrFdX3UsGy96VO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-krPrFdX3UsGy96VO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-krPrFdX3UsGy96VO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-krPrFdX3UsGy96VO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-krPrFdX3UsGy96VO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-krPrFdX3UsGy96VO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-krPrFdX3UsGy96VO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-krPrFdX3UsGy96VO .marker.cross{stroke:#333333;}#mermaid-svg-krPrFdX3UsGy96VO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-krPrFdX3UsGy96VO p{margin:0;}#mermaid-svg-krPrFdX3UsGy96VO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-krPrFdX3UsGy96VO .cluster-label text{fill:#333;}#mermaid-svg-krPrFdX3UsGy96VO .cluster-label span{color:#333;}#mermaid-svg-krPrFdX3UsGy96VO .cluster-label span p{background-color:transparent;}#mermaid-svg-krPrFdX3UsGy96VO .label text,#mermaid-svg-krPrFdX3UsGy96VO span{fill:#333;color:#333;}#mermaid-svg-krPrFdX3UsGy96VO .node rect,#mermaid-svg-krPrFdX3UsGy96VO .node circle,#mermaid-svg-krPrFdX3UsGy96VO .node ellipse,#mermaid-svg-krPrFdX3UsGy96VO .node polygon,#mermaid-svg-krPrFdX3UsGy96VO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-krPrFdX3UsGy96VO .rough-node .label text,#mermaid-svg-krPrFdX3UsGy96VO .node .label text,#mermaid-svg-krPrFdX3UsGy96VO .image-shape .label,#mermaid-svg-krPrFdX3UsGy96VO .icon-shape .label{text-anchor:middle;}#mermaid-svg-krPrFdX3UsGy96VO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-krPrFdX3UsGy96VO .rough-node .label,#mermaid-svg-krPrFdX3UsGy96VO .node .label,#mermaid-svg-krPrFdX3UsGy96VO .image-shape .label,#mermaid-svg-krPrFdX3UsGy96VO .icon-shape .label{text-align:center;}#mermaid-svg-krPrFdX3UsGy96VO .node.clickable{cursor:pointer;}#mermaid-svg-krPrFdX3UsGy96VO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-krPrFdX3UsGy96VO .arrowheadPath{fill:#333333;}#mermaid-svg-krPrFdX3UsGy96VO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-krPrFdX3UsGy96VO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-krPrFdX3UsGy96VO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-krPrFdX3UsGy96VO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-krPrFdX3UsGy96VO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-krPrFdX3UsGy96VO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-krPrFdX3UsGy96VO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-krPrFdX3UsGy96VO .cluster text{fill:#333;}#mermaid-svg-krPrFdX3UsGy96VO .cluster span{color:#333;}#mermaid-svg-krPrFdX3UsGy96VO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-krPrFdX3UsGy96VO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-krPrFdX3UsGy96VO rect.text{fill:none;stroke-width:0;}#mermaid-svg-krPrFdX3UsGy96VO .icon-shape,#mermaid-svg-krPrFdX3UsGy96VO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-krPrFdX3UsGy96VO .icon-shape p,#mermaid-svg-krPrFdX3UsGy96VO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-krPrFdX3UsGy96VO .icon-shape .label rect,#mermaid-svg-krPrFdX3UsGy96VO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-krPrFdX3UsGy96VO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-krPrFdX3UsGy96VO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-krPrFdX3UsGy96VO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 单元测试最佳实践
测试命名清晰
单一职责
合理使用Mock
持续集成
test_add_正数相加_返回8
一个测试验证一个行为
只Mock外部依赖
集成到CI/CD流水线
最佳实践详解:
| 最佳实践 | 说明 |
|---|---|
| 测试命名清晰 | 测试方法名应清晰表达测试意图,如 test_divide_除数为零_抛出异常 |
| 单一职责 | 每个测试用例只验证一个行为,失败时能快速定位 |
| 合理使用Mock | 只Mock不稳定的外部依赖,不要Mock所有东西 |
| 测试数据独立 | 每个测试用例使用独立的测试数据,互不干扰 |
| 持续集成 | 单元测试集成到CI/CD,每次提交自动运行 |
| 定期维护测试 | 代码修改时同步更新测试,保持测试有效性 |
| 关注边界和异常 | 不要只测正常路径,边界值和异常场景更重要 |
| 代码审查包含测试 | Review代码时同时Review测试代码 |
(四)单元测试的检查清单
测试检查清单:
| 检查项 | 标准 | 说明 |
|---|---|---|
| ✅ 自动化 | 无需人工干预即可运行 | 符合AIR原则 |
| ✅ 独立性 | 用例之间互不依赖 | 可以单独运行任意用例 |
| ✅ 可重复 | 任何环境都得到相同结果 | 不依赖本地环境 |
| ✅ 快速 | 单个用例毫秒级执行 | 整体控制在秒级 |
| ✅ 有断言 | 每个测试至少一个有效断言 | 不是只调用不验证 |
| ✅ 命名清晰 | 一眼能看出测试什么 | 失败时能快速理解 |
| ✅ 结构清晰 | 遵循AAA模式 | Arrange-Act-Assert |
| ✅ 覆盖边界 | 包含边界值和异常输入 | 不只测正常路径 |
📝 总结
单元测试是保障软件质量的基石,是测试金字塔最底层的支撑。
🎯 核心理念:单元测试针对最小可测试单元进行独立验证,由开发者编写,在编码阶段执行;其本质是"隔离"------把被测单元与外部依赖隔离,单独验证其行为。
📦 设计原则:遵循AIR原则(自动化、独立性、可重复)和BCDE原则(边界、正确、设计、错误);使用AAA模式组织测试代码(准备-执行-断言)。
🔧 方法技术:静态测试包括代码审查、静态分析;动态测试包括逻辑覆盖(语句/判定/条件/路径覆盖)和基本路径测试;现代开发借助JUnit、pytest、Jest等测试框架编写和执行。
🔄 TDD驱动:测试驱动开发遵循Red-Green-Refactor循环------先写失败测试,再写最少代码通过测试,最后重构优化;虽然初期投入大,但长期收益显著。
📊 Mock隔离:通过Test Double(Dummy/Stub/Spy/Mock/Fake)替代外部依赖,实现真正的单元测试隔离。
📈 覆盖率:语句覆盖率、分支覆盖率、条件覆盖率是核心指标;覆盖率是下限而非上限,测试质量比数字更重要。
💡 最佳实践:测试命名清晰、单一职责、合理使用Mock、集成CI/CD、关注边界和异常、定期维护测试代码。
核心启示 :单元测试不是"可选的附加任务",而是"必须的核心工作"。很多团队在时间紧张时首先砍掉单元测试,殊不知这恰恰是在透支未来------没有单元测试的代码就像没有地基的建筑,看似建得快,但任何修改都可能引发坍塌。写好单元测试的开发者,不是因为他们有更多时间,而是因为他们知道:今天多花10分钟写测试,未来可以少花10小时排查bug。 单元测试的真正价值不仅是发现缺陷,更是塑造更好的代码设计。当你被迫为一个类编写单元测试时,你会自然地思考:这个类职责是否过多?依赖是否过于复杂?接口是否清晰?------这正是单元测试倒逼良好设计的力量。