diff --git a/yuwenhao/Library/pictures/6.png b/yuwenhao/Library/pictures/6.png new file mode 100644 index 0000000..b30afd8 Binary files /dev/null and b/yuwenhao/Library/pictures/6.png differ diff --git a/yuwenhao/UnitTest/UnitTestVisualStudio.md b/yuwenhao/UnitTest/UnitTestVisualStudio.md index e69de29..aaf0711 100644 --- a/yuwenhao/UnitTest/UnitTestVisualStudio.md +++ b/yuwenhao/UnitTest/UnitTestVisualStudio.md @@ -0,0 +1,1459 @@ +# VS2019 单元测试学习文档 + +## 1. 学习目标 + +本篇文档用于学习在 Visual Studio 2019 平台下进行 C/C++ 单元测试的基本方法。学习重点包括单元测试创建、测试样例书写、测试运行、代码覆盖度、运行性能分析以及单元测试调试。 + +通过本章学习,应达到以下目标: + +1. 理解单元测试的基本概念和作用; +2. 能够在 VS2019 中创建单元测试工程; +3. 能够编写基本测试样例; +4. 能够通过测试资源管理器运行测试; +5. 能够查看测试结果和定位失败原因; +6. 能够分析代码覆盖度; +7. 能够观察单元测试运行性能; +8. 能够对单元测试进行断点调试。 + +## 2. 单元测试基本概念 + +### 2.1 什么是单元测试 + +单元测试是指对程序中最小的可测试单元进行验证。对于 C/C++ 程序来说,一个单元通常可以是一个函数、一个类、一个模块或一个接口。 + +单元测试的核心目的是:在程序整体运行之前,先验证局部功能是否正确。 + +例如有一个加法函数: + +```cpp +int Add(int a, int b) +{ + return a + b; +} +``` + +可以针对该函数编写测试: + +```cpp +Assert::AreEqual(3, Add(1, 2)); +``` + +如果 `Add(1, 2)` 的结果为 `3`,测试通过;如果结果不是 `3`,测试失败。 + +### 2.2 单元测试和普通运行的区别 + +普通运行通常是从 `main()` 函数开始,按照完整业务流程执行程序。单元测试则是直接调用某个具体函数或模块,验证这个小功能是否满足预期。 + +| 对比项 | 普通运行 | 单元测试 | +|---|---|---| +| 入口 | `main()` 函数 | 测试框架自动执行测试函数 | +| 目标 | 验证整个程序能否运行 | 验证某个函数或模块是否正确 | +| 关注点 | 整体流程 | 局部功能 | +| 结果判断 | 人工观察输出较多 | 通过断言自动判断 | +| 适用场景 | 功能联调、整体运行 | 函数验证、回归测试、边界测试 | + +单元测试并不是替代普通运行,而是补充普通运行。它可以在功能还没有完全联调之前,先验证底层函数和模块是否可靠。 + +### 2.3 单元测试的意义 + +单元测试的意义主要体现在以下几个方面: + +1. **提前发现问题**:函数刚写完就能验证,不需要等到整个项目联调; +2. **防止修改引入新问题**:后续修改代码后重新运行测试,可以判断原有功能是否被破坏; +3. **提高代码可靠性**:重要逻辑通过多组样例验证后,代码可信度更高; +4. **方便定位错误**:测试失败时,通常可以直接定位到某个函数或某个条件; +5. **形成可重复验证手段**:测试样例可以长期保留,后续每次改代码都能复测。 + +对于嵌入式软件开发来说,虽然最终代码可能运行在单片机上,但很多纯算法、协议解析、数据处理、状态机逻辑都可以先在 PC 上用 VS2019 做单元测试。 + +## 3. VS2019 单元测试环境准备 + +### 3.1 安装和组件要求 + +在 VS2019 中进行 C++ 单元测试,建议安装以下组件: + +1. Visual Studio 2019; +2. 使用 C++ 的桌面开发; +3. MSVC 编译工具; +4. Windows SDK; +5. C++ 单元测试相关组件。 + +如果创建项目时找不到 C++ 单元测试模板,可以打开 Visual Studio Installer,检查是否安装了“使用 C++ 的桌面开发”工作负载。 + +### 3.2 常用窗口 + +进行单元测试时常用的 VS2019 窗口如下: + +| 窗口 | 作用 | +|---|---| +| 解决方案资源管理器 | 查看主程序项目和测试项目 | +| 测试资源管理器 | 运行测试、查看测试结果 | +| 错误列表 | 查看编译错误和链接错误 | +| 输出窗口 | 查看生成、测试输出信息 | +| 代码覆盖率结果 | 查看代码覆盖率结果 | +| 监视窗口 | 调试测试时查看变量 | +| 调用堆栈窗口 | 调试测试时查看函数调用关系 | + +打开测试资源管理器: + +```text +测试 -> 测试资源管理器 +``` + +![测试资源管理器窗口](pictures/6.png) + +## 4. 单元测试工程创建 + +### 4.1 推荐工程结构 + +在 VS2019 中学习单元测试时,建议使用“被测代码项目 + 单元测试项目”的结构。 + +推荐结构如下: + +```text +解决方案 UnitTestDemo +|---CalculatorLib 被测代码项目,生成静态库 +| |---Calculator.h +| +---Calculator.cpp +| ++---CalculatorTest 单元测试项目 + +---UnitTest1.cpp +``` + +其中: + +1. `CalculatorLib` 保存真正要测试的代码; +2. `CalculatorTest` 保存测试代码; +3. 测试项目调用被测项目中的函数; +4. 被测代码尽量不要直接写在 `main.cpp` 中,而应拆分成 `.h` 和 `.cpp` 文件,方便测试。 + +![单元测试推荐工程结构](pictures/8.png) + +### 4.2 创建被测代码项目 + +第一步先创建一个被测代码项目,用来保存需要测试的函数或模块。 + +操作步骤: + +1. 打开 VS2019; +2. 点击“创建新项目”; +3. 选择 C++ 项目模板; +4. 可以选择“静态库”或“空项目”; +5. 项目名称填写 `CalculatorLib`; +6. 解决方案名称填写 `UnitTestDemo`; +7. 点击“创建”。 + +如果创建的是空项目,需要手动添加 `.h` 和 `.cpp` 文件。 + +### 4.3 添加被测函数 + +在 `CalculatorLib` 项目中添加头文件 `Calculator.h`: + +```cpp +#ifndef CALCULATOR_H +#define CALCULATOR_H + +int Add(int a, int b); +int Sub(int a, int b); +int Mul(int a, int b); +int Div(int a, int b); + +#endif +``` + +添加源文件 `Calculator.cpp`: + +```cpp +#include "Calculator.h" +#include + +int Add(int a, int b) +{ + return a + b; +} + +int Sub(int a, int b) +{ + return a - b; +} + +int Mul(int a, int b) +{ + return a * b; +} + +int Div(int a, int b) +{ + if (b == 0) + { + throw std::invalid_argument("divisor can not be zero"); + } + + return a / b; +} +``` + +该模块中包含加法、减法、乘法和除法函数。除法函数中增加了除数为 0 的异常处理,用于后续编写异常测试样例。 + +![添加被测函数](pictures/9.png) + +### 4.4 创建单元测试项目 + +在同一个解决方案中添加单元测试项目。 + +操作步骤: + +1. 右键点击解决方案; +2. 选择“添加”; +3. 选择“新建项目”; +4. 搜索“测试”; +5. 选择“本机单元测试项目”; +6. 项目名称填写 `CalculatorTest`; +7. 点击“创建”。 + +创建完成后,解决方案中会出现两个项目: + +```text +CalculatorLib +CalculatorTest +``` + +![创建本机单元测试项目](pictures/7.png) + +### 4.5 配置测试项目调用被测项目 + +测试项目要调用被测项目中的函数,需要让测试项目找到头文件和库文件。 + +常用配置方式如下。 + +#### 4.5.1 配置头文件路径 + +右键测试项目 `CalculatorTest`,打开属性页: + +```text +项目属性 -> 配置属性 -> C/C++ -> 常规 -> 附加包含目录 +``` + +添加被测项目头文件所在目录,例如: + +```text +..\CalculatorLib +``` + +作用是让测试代码可以写: + +```cpp +#include "Calculator.h" +``` + +#### 4.5.2 配置库文件路径 + +如果被测项目生成的是静态库,需要配置 `.lib` 文件所在目录。 + +路径: + +```text +项目属性 -> 配置属性 -> 链接器 -> 常规 -> 附加库目录 +``` + +可以填写被测项目的输出目录,例如: + +```text +..\Debug +``` + +或者根据实际输出目录填写: + +```text +..\x64\Debug +``` + +需要注意,Debug/Release、Win32/x64 不同配置下输出目录可能不同。 + +#### 4.5.3 配置附加依赖项 + +路径: + +```text +项目属性 -> 配置属性 -> 链接器 -> 输入 -> 附加依赖项 +``` + +添加被测项目生成的 `.lib` 文件名,例如: + +```text +CalculatorLib.lib +``` + +作用是告诉链接器要链接哪个库文件。 + +#### 4.5.4 设置项目依赖关系 + +为了保证测试项目生成前,先生成被测项目,可以设置项目依赖关系。 + +操作路径: + +```text +右键解决方案 -> 项目依赖项 +``` + +在项目中选择 `CalculatorTest`,然后勾选它依赖的 `CalculatorLib`。 + +这样重新生成解决方案时,VS 会先生成 `CalculatorLib`,再生成 `CalculatorTest`。 + +## 5. 测试样例书写 + +### 5.1 单元测试文件基本结构 + +VS2019 本机 C++ 单元测试常用框架为 Microsoft C++ Unit Test Framework。测试文件通常包含以下结构: + +```cpp +#include "pch.h" +#include "CppUnitTest.h" +#include "Calculator.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace CalculatorTest +{ + TEST_CLASS(CalculatorBasicTest) + { + public: + TEST_METHOD(Add_1And2_Return3) + { + int ret = Add(1, 2); + Assert::AreEqual(3, ret); + } + }; +} +``` + +其中: + +| 结构 | 含义 | +|---|---| +| `TEST_CLASS` | 定义一个测试类 | +| `TEST_METHOD` | 定义一个测试方法 | +| `Assert::AreEqual` | 判断实际结果是否等于期望结果 | +| `namespace` | 测试代码命名空间,用于组织测试 | + +![单元测试代码基本结构](pictures/15.png) + +### 5.2 测试样例命名规范 + +测试样例名称应尽量清晰,最好能看出:测试对象、测试条件和预期结果。 + +推荐命名方式: + +```text +被测函数_测试场景_预期结果 +``` + +例如: + +```text +Add_1And2_Return3 +Div_10And2_Return5 +Div_DivisorIsZero_ThrowException +``` + +不推荐使用过于简单的名称: + +```text +Test1 +Test2 +FuncTest +``` + +清晰的测试名称可以帮助快速判断失败样例对应的功能。 + +### 5.3 AAA 测试结构 + +单元测试通常按照 AAA 结构书写: + +```text +Arrange:准备测试数据 +Act :执行被测函数 +Assert :判断结果是否正确 +``` + +示例: + +```cpp +TEST_METHOD(Add_1And2_Return3) +{ + // Arrange + int a = 1; + int b = 2; + + // Act + int ret = Add(a, b); + + // Assert + Assert::AreEqual(3, ret); +} +``` + +这种写法结构清晰,便于后续维护和排查问题。 + +### 5.4 常用断言方法 + +断言是单元测试中判断结果是否正确的核心语句。常用断言如下: + +| 断言 | 作用 | +|---|---| +| `Assert::AreEqual(expected, actual)` | 判断两个值是否相等 | +| `Assert::AreNotEqual(notExpected, actual)` | 判断两个值是否不相等 | +| `Assert::IsTrue(condition)` | 判断条件是否为真 | +| `Assert::IsFalse(condition)` | 判断条件是否为假 | +| `Assert::IsNull(pointer)` | 判断指针是否为空 | +| `Assert::IsNotNull(pointer)` | 判断指针是否非空 | +| `Assert::Fail()` | 主动让测试失败 | +| `Assert::ExpectException()` | 判断是否抛出指定异常 | + +示例: + +```cpp +TEST_METHOD(Div_DivisorIsZero_ThrowException) +{ + Assert::ExpectException([] { + Div(10, 0); + }); +} +``` + +### 5.5 正常值测试 + +正常值测试用于验证函数在常规输入下是否工作正确。 + +示例: + +```cpp +TEST_METHOD(Add_1And2_Return3) +{ + Assert::AreEqual(3, Add(1, 2)); +} + +TEST_METHOD(Sub_5And3_Return2) +{ + Assert::AreEqual(2, Sub(5, 3)); +} + +TEST_METHOD(Mul_3And4_Return12) +{ + Assert::AreEqual(12, Mul(3, 4)); +} + +TEST_METHOD(Div_10And2_Return5) +{ + Assert::AreEqual(5, Div(10, 2)); +} +``` + +![边界值测试](pictures/19.png) + +正常值测试是最基础的测试,但只写正常值是不够的,还需要覆盖边界和异常情况。 + +### 5.6 边界值测试 + +边界值测试用于验证函数在特殊边界输入下是否正确。 + +例如对加法函数,可以测试 0、负数、最大值附近等情况。 + +```cpp +TEST_METHOD(Add_ZeroAndZero_ReturnZero) +{ + Assert::AreEqual(0, Add(0, 0)); +} + +TEST_METHOD(Add_NegativeNumber_ReturnCorrectValue) +{ + Assert::AreEqual(-3, Add(-1, -2)); +} + +TEST_METHOD(Sub_ResultIsNegative_ReturnCorrectValue) +{ + Assert::AreEqual(-1, Sub(2, 3)); +} +``` + +边界值测试可以发现很多普通输入无法发现的问题。 + +![边界值测试](pictures/21.png) + +### 5.7 异常测试 + +对于可能出现错误输入的函数,应编写异常测 试。 + +例如除法函数中,除数为 0 时应抛出异常: + +```cpp +TEST_METHOD(Div_DivisorIsZero_ThrowException) +{ + Assert::ExpectException([] { + Div(10, 0); + }); +} +``` + +异常测试的意义是验证程序不仅在正常情况下正确,也能在异常输入下给出合理处理。 + +![异常测试](pictures/20.png) + +### 5.8 测试初始化和清理 + +如果多个测试方法需要相同的初始化操作,可以使用初始化函数。如果测试完成后需要释放资源,可以使用清理函数。 + +示例: + +```cpp +TEST_CLASS(CalculatorBasicTest) +{ +public: + TEST_METHOD_INITIALIZE(MethodInit) + { + Logger::WriteMessage("Before each test method"); + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + Logger::WriteMessage("After each test method"); + } + + TEST_METHOD(Add_1And2_Return3) + { + Assert::AreEqual(3, Add(1, 2)); + } +}; +``` + +常见初始化和清理宏如下: + +| 宏 | 作用 | +|---|---| +| `TEST_METHOD_INITIALIZE` | 每个测试方法执行前运行 | +| `TEST_METHOD_CLEANUP` | 每个测试方法执行后运行 | +| `TEST_CLASS_INITIALIZE` | 测试类中所有测试执行前运行一次 | +| `TEST_CLASS_CLEANUP` | 测试类中所有测试执行后运行一次 | + +![测试初始化和清理](pictures/22.png) + +### 5.9 完整测试代码示例 + +`UnitTest1.cpp` 示例: + +```cpp +#include "pch.h" +#include "CppUnitTest.h" +#include "Calculator.h" +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace CalculatorTest +{ + TEST_CLASS(CalculatorBasicTest) + { + public: + TEST_METHOD(Add_1And2_Return3) + { + Assert::AreEqual(3, Add(1, 2)); + } + + TEST_METHOD(Sub_5And3_Return2) + { + Assert::AreEqual(2, Sub(5, 3)); + } + + TEST_METHOD(Mul_3And4_Return12) + { + Assert::AreEqual(12, Mul(3, 4)); + } + + TEST_METHOD(Div_10And2_Return5) + { + Assert::AreEqual(5, Div(10, 2)); + } + + TEST_METHOD(Div_DivisorIsZero_ThrowException) + { + Assert::ExpectException([] { + Div(10, 0); + }); + } + }; +} +``` + +该测试文件覆盖了四则运算的正常输入和除数为 0 的异常输入。 + +![完整测试代码示例](pictures/23.png) + +## 6. 运行单元测试 + +### 6.1 生成解决方案 + +测试运行前应先生成解决方案。 + +操作方式: + +```text +生成 -> 生成解决方案 +``` + +快捷键: + +```text +Ctrl + Shift + B +``` + +如果生成失败,应先查看错误列表,解决编译错误或链接错误。 + +![生成解决方案](pictures/24.png) + +### 6.2 打开测试资源管理器 + +测试资源管理器是 VS2019 中运行和查看测试结果的主要窗口。 + +打开路径: + +```text +测试 -> 测试资源管理器 +``` + +测试项目生成成功后,测试资源管理器中会自动发现 `TEST_METHOD` 定义的测试方法。 + +如果测试资源管理器中没有显示测试,需要检查: + +1. 测试项目是否生成成功; +2. 测试方法是否使用了 `TEST_METHOD`; +3. 测试类是否使用了 `TEST_CLASS`; +4. 测试文件是否属于测试项目; +5. 当前平台和配置是否正确。 + +### 6.3 运行所有测试 + +运行所有测试的方法: + +```text +测试资源管理器 -> 运行全部 +``` + +运行后可以看到每个测试的结果状态。 + +常见状态如下: + +| 状态 | 含义 | +|---|---| +| 通过 | 测试执行成功,断言全部满足 | +| 失败 | 测试执行完成,但断言不满足或出现异常 | +| 跳过 | 测试被跳过,没有执行 | +| 未运行 | 测试还没有执行 | + +![运行所有测试](pictures/26.png) + +### 6.4 运行单个测试 + +如果只想验证某一个测试样例,可以右键指定测试方法,然后选择运行。 + +操作方法: + +```text +测试资源管理器 -> 右键某个测试 -> 运行 +``` + +适合场景: + +1. 只修改了某个函数; +2. 只想验证一个失败样例; +3. 调试前先单独运行目标测试; +4. 测试数量较多时减少等待时间。 + +![运行单个测试](pictures/27.png) + +### 6.5 查看测试失败原因 + +测试失败时,应查看失败详情。 + +一般可以看到: + +1. 失败的测试方法名称; +2. 失败原因; +3. 期望值和实际值; +4. 失败位置; +5. 异常信息。 + +例如断言失败: + +```text +Assert failed. Expected:<3>. Actual:<4>. +``` + +这说明测试期望结果是 `3`,但实际执行结果是 `4`。 + +![查看测试失败详情](pictures/28.png) + +## 7. 代码覆盖度 + +代码覆盖度用于统计单元测试运行过程中,被测代码中有多少代码被执行到了。它可以帮助开发人员判断测试用例是否覆盖了主要功能逻辑,是否存在没有被测试到的分支、函数或代码行。 + +需要注意的是,代码覆盖度并不是判断程序是否正确的唯一标准。覆盖率高只能说明测试执行到了更多代码,但不代表测试用例一定设计合理。因此,代码覆盖度应该和测试样例设计一起使用。 + +### 7.1 代码覆盖度的基本概念 + +代码覆盖度主要用于回答以下问题: + +1. 哪些函数被测试到了; +2. 哪些代码行被执行到了; +3. 哪些分支条件被覆盖到了; +4. 哪些代码虽然写了,但测试时从未执行; +5. 当前测试用例是否充分。 + +常见覆盖率指标如下: + +| 覆盖率类型 | 含义 | +| ---------- | ------------------------------------------------ | +| 行覆盖率 | 统计代码中有多少行被执行过 | +| 函数覆盖率 | 统计有多少函数被调用过 | +| 分支覆盖率 | 统计 `if`、`else`、`switch` 等分支是否都被执行过 | +| 块覆盖率 | 统计基本代码块是否被执行过 | + +例如下面代码: + +```cpp +int Max(int a, int b) +{ + if (a > b) + { + return a; + } + else + { + return b; + } +} +``` + +如果测试用例只有: + +```cpp +Assert::AreEqual(5, Max(5, 3)); +``` + +那么程序只执行了 `a > b` 成立的分支,没有执行 `else` 分支。此时代码虽然测试通过,但覆盖率并不完整。 + +如果再添加一个测试用例: + +```cpp +Assert::AreEqual(6, Max(2, 6)); +``` + +那么 `if` 和 `else` 两个分支都被测试到了,测试覆盖度会更高。 + +### 7.2 VS2019 Community 中的代码覆盖度工具 + +VS2019 Enterprise 版本自带代码覆盖率分析功能,但是 VS2019 Community 版本通常没有自带完整的代码覆盖率功能。因此,在 Community 版本中,可以安装第三方插件来辅助查看代码覆盖度。 + +本次学习使用的插件为: + +```text +Fine Code Coverage +``` + +Fine Code Coverage 安装完成后,一般不会在项目右键菜单中显示“运行覆盖率”按钮,而是通过单独的覆盖率窗口显示结果。 + +打开方式为: + +```text +视图 -> 其他窗口 -> Fine Code Coverage +``` + +如果菜单中没有看到该选项,也可以在 VS2019 顶部搜索框中输入: + +```text +Fine Code Coverage +``` + +然后打开对应窗口。 + +### 7.3 安装并检查 Fine Code Coverage 插件 + +安装 Fine Code Coverage 的步骤如下: + +1. 打开 VS2019; +2. 点击菜单栏: + + ```text + 扩展 -> 管理扩展 + ``` + +3. 在左侧选择: + + ```text + 联机 + ``` + +4. 搜索: + + ```text + Fine Code Coverage + ``` + +5. 点击下载并安装; +6. 关闭 VS2019,等待 VSIX Installer 完成安装; +7. 重新打开 VS2019。 + +安装完成后,可以通过下面路径检查是否安装成功: + +```text +扩展 -> 管理扩展 -> 已安装 +``` + +如果能够看到: + +```text +Fine Code Coverage +``` + +说明插件已经安装成功。 + +![检查Fine Code Coverage插件](pictures/3.png) + +### 7.4 运行代码覆盖度的前提条件 + +运行代码覆盖度之前,需要先保证单元测试项目本身可以正常运行。如果单元测试都无法通过,代码覆盖率统计也没有意义。 + +运行覆盖度前应检查以下内容: + +1. 当前配置建议使用 `Debug`; +2. 平台需要保持一致,例如都使用 `Win32`; +3. 被测项目能够正常生成; +4. 单元测试项目能够正常生成; +5. 测试资源管理器中能够识别测试用例; +6. 单元测试可以正常运行并通过。 + +项目结构通常如下: + +```text +UnitTestDemo +|---CalculatorLib 被测试代码项目 +| |---Calculator.h +| +---Calculator.cpp +| ++---CalculatorTest 单元测试项目 + +---UnitTest1.cpp +``` + +其中: + +| 项目 | 作用 | +| ---------------- | -------------------- | +| `CalculatorLib` | 保存真正要测试的代码 | +| `CalculatorTest` | 保存单元测试代码 | +| `UnitTest1.cpp` | 编写测试样例 | + +代码覆盖度统计时,实际运行的是测试项目 `CalculatorTest`,统计的是被测代码项目 `CalculatorLib` 中有多少代码被执行到。 + +可以简单理解为: + +```text +运行测试项目,统计被测项目。 +``` + +### 7.5 使用 Fine Code Coverage 运行覆盖度 + +使用 Fine Code Coverage 查看代码覆盖度的基本步骤如下: + +1. 重新生成解决方案: + + ```text + 生成 -> 重新生成解决方案 + ``` + +2. 打开测试资源管理器: + + ```text + 测试 -> 窗口 -> 测试资源管理器 + ``` + +3. 打开 Fine Code Coverage 窗口: + + ```text + 视图 -> 其他窗口 -> Fine Code Coverage + ``` + ![](pictures\代码覆盖率.png) + +4. 在测试资源管理器中点击: + + ```text + 运行所有测试 + ``` + +5. 测试运行完成后,查看 Fine Code Coverage 窗口中的覆盖率结果。 + +如果插件正常工作,窗口中会显示代码覆盖率统计信息,例如文件覆盖率、行覆盖率、分支覆盖率等内容。 + +![Fine Code Coverage覆盖率结果](pictures/覆盖率结果.png) + +### 7.6 查看代码覆盖度结果 + +代码覆盖度运行完成后,Fine Code Coverage 窗口通常会显示被测代码的覆盖情况。 + +常见内容包括: + +| 内容 | 含义 | +| --------------- | ------------------ | +| Covered | 已覆盖的代码 | +| Uncovered | 未覆盖的代码 | +| Coverable | 可统计覆盖率的代码 | +| Line Coverage | 行覆盖率 | +| Branch Coverage | 分支覆盖率 | +| Method Coverage | 方法或函数覆盖率 | + +例如,一个函数中有 10 行可执行代码,单元测试运行时执行了其中 8 行,则行覆盖率约为: + +```text +8 / 10 = 80% +``` + +如果发现某些代码显示为未覆盖,说明当前测试用例没有执行到这些代码。此时应分析是否需要补充新的测试样例。 + +### 7.7 通过覆盖率结果补充测试用例 + +代码覆盖度的主要作用不是单纯追求百分比,而是帮助发现测试不充分的地方。 + +例如有如下函数: + +```cpp +int Divide(int a, int b) +{ + if (b == 0) + { + return 0; + } + + return a / b; +} +``` + +如果只写了下面测试: + +```cpp +TEST_METHOD(TestDivideNormal) +{ + Assert::AreEqual(2, Divide(6, 3)); +} +``` + +那么只测试了正常除法,没有测试 `b == 0` 的异常分支。 + +为了提高覆盖度,需要补充测试: + +```cpp +TEST_METHOD(TestDivideByZero) +{ + Assert::AreEqual(0, Divide(6, 0)); +} +``` + +这样可以让 `if (b == 0)` 分支也被执行到。 + +覆盖率分析的基本流程如下: + +```text +运行测试 +查看未覆盖代码 +分析未覆盖原因 +补充测试样例 +再次运行测试 +观察覆盖率变化 +``` + +### 7.8 代码覆盖度和测试质量的关系 + +代码覆盖度高,不代表测试质量一定高。 + +例如下面测试虽然执行了函数,但测试意义不强: + +```cpp +TEST_METHOD(TestAdd) +{ + Add(1, 2); +} +``` + +这个测试只是调用了函数,没有判断结果是否正确。即使覆盖率增加了,也不能证明函数运行结果正确。 + +更合理的写法是: + +```cpp +TEST_METHOD(TestAdd) +{ + Assert::AreEqual(3, Add(1, 2)); +} +``` + +因此,好的单元测试应该同时满足: + +1. 能够执行被测代码; +2. 能够判断结果是否正确; +3. 覆盖正常情况; +4. 覆盖边界情况; +5. 覆盖异常情况; +6. 测试结果可重复。 + +覆盖率只能说明代码是否被执行,不能说明测试断言是否合理。 + +总结如下: + +```text +覆盖率高,不一定测试质量高; +覆盖率低,通常说明测试不充分。 +``` + +### 7.9 Fine Code Coverage 使用注意事项 + +在 VS2019 Community 中使用 Fine Code Coverage 时,需要注意以下问题: + +1. Fine Code Coverage 更常用于 .NET 项目,对 C++ 本机单元测试项目的支持可能不如专门的 C++ 覆盖率工具稳定; +2. 如果窗口中没有结果,应先确认测试资源管理器中测试能正常运行; +3. 建议使用 Debug 配置运行覆盖率; +4. 被测项目和测试项目的平台要保持一致,例如都为 Win32; +5. 如果测试没有被识别,覆盖率窗口也不会显示有效结果; +6. 如果 C++ 项目覆盖率结果异常,可以考虑使用 OpenCppCoverage 作为补充工具。 + +如果 Fine Code Coverage 没有显示结果,可以按以下顺序排查: + +```text +1. 检查 Fine Code Coverage 是否安装成功; +2. 检查是否打开了 Fine Code Coverage 窗口; +3. 检查测试资源管理器中是否能看到测试用例; +4. 检查单元测试是否能运行通过; +5. 检查当前是否为 Debug 配置; +6. 检查 Win32/x64 平台是否一致; +7. 重新生成解决方案后再次运行测试。 +``` + +### 7.10 代码覆盖度学习总结 + +代码覆盖度是单元测试中的重要辅助指标,用于判断测试运行时执行到了哪些代码。通过覆盖率结果,可以发现未被测试到的函数、语句和分支,从而补充更加完整的测试样例。 + +在 VS2019 Community 中,由于没有自带完整的代码覆盖率功能,可以使用 Fine Code Coverage 插件辅助查看覆盖率。使用时需要先打开 Fine Code Coverage 窗口,再运行单元测试,最后在覆盖率窗口中查看结果。 + +本节学习重点如下: + +| 学习内容 | 说明 | +| ------------------ | ----------------------------------- | +| 代码覆盖度概念 | 判断测试执行到了多少代码 | +| 行覆盖率 | 判断代码行是否被执行 | +| 分支覆盖率 | 判断条件分支是否被覆盖 | +| Fine Code Coverage | VS2019 Community 中常用的覆盖率插件 | +| 测试资源管理器 | 用于运行单元测试 | +| 覆盖率结果分析 | 根据未覆盖代码补充测试样例 | + +一句话总结: + +```text +代码覆盖度不是为了单纯追求百分比,而是为了发现测试没有覆盖到的代码,从而完善测试样例。 +``` + +## 8. 运行性能 + +### 8.1 单元测试运行性能的含义 + +单元测试运行性能是指测试样例执行所消耗的时间。测试运行时间过长,会影响开发效率,也可能说明被测函数存在性能问题。 + +在 VS2019 中,测试资源管理器可以显示测试运行耗时。通过耗时可以初步判断哪些测试比较慢。 + +### 8.2 查看测试运行时间 + +操作步骤: + +1. 打开测试资源管理器; +2. 运行全部测试或指定测试; +3. 查看测试结果列表中的运行时间; +4. 点击某个测试查看详细结果。 + +测试运行时间可以帮助判断: + +1. 哪些测试执行较慢; +2. 修改代码后性能是否明显变差; +3. 是否存在不合理循环或阻塞操作; +4. 是否有测试依赖外部资源导致耗时过长。 + +![查看测试运行时间](pictures/35.png) + +### 8.3 编写简单性能测试样例 + +对于简单函数,可以通过多次循环调用来观察执行耗时。 + +示例: + +```cpp +#include + +TEST_METHOD(Add_Performance_OneMillionTimes) +{ + auto start = std::chrono::high_resolution_clock::now(); + + int sum = 0; + for (int i = 0; i < 1000000; i++) + { + sum += Add(i, 1); + } + + auto end = std::chrono::high_resolution_clock::now(); + auto cost = std::chrono::duration_cast(end - start).count(); + + Logger::WriteMessage((L"cost = " + std::to_wstring(cost) + L" ms").c_str()); + + Assert::IsTrue(sum != 0); +} +``` + +需要注意,普通单元测试中的性能测试只能作为简单参考,因为电脑负载、Debug/Release、优化等级都会影响耗时。 + +![编写简单性能测试](pictures/36.png) + +### 8.4 性能测试注意事项 + +性能测试时需要注意: + +1. 不要把偶然一次运行时间当成绝对结论; +2. Debug 模式下速度通常比 Release 慢; +3. 电脑后台程序会影响测试耗时; +4. 测试样例中不要加入不必要的打印; +5. 性能测试和正确性测试最好分开管理; +6. 性能测试应关注趋势,而不是只关注单次结果。 + +对于训练营学习来说,重点是掌握如何查看测试运行时间,并理解运行性能对测试效率的影响。 + +## 9. 单元测试的调试 + +### 9.1 为什么要调试单元测试 + +当单元测试失败时,不能只看失败结果,还需要进入代码内部分析原因。单元测试调试可以帮助查看: + +1. 测试输入是否正确; +2. 被测函数是否被调用; +3. 中间变量是否符合预期; +4. 程序是否进入了正确分支; +5. 异常是否按预期抛出。 + +### 9.2 给测试方法设置断点 + +可以直接在测试方法中设置断点。 + +例如: + +```cpp +TEST_METHOD(Add_1And2_Return3) +{ + int ret = Add(1, 2); + Assert::AreEqual(3, ret); +} +``` + +在 `Assert::AreEqual(3, ret);` 这一行左侧点击,出现红色断点。 + +![给测试方法设置断点](pictures/39.png) + +### 9.3 调试所选测试 + +调试单个测试的方法: + +```text +测试资源管理器 -> 右键测试方法 -> 调试 +``` + +程序会进入调试状态,并停在设置的断点处。 + +![调试所选测试](pictures/40.png) + +### 9.4 单步进入被测函数 + +调试时常用快捷键: + +| 操作 | 快捷键 | 说明 | +|---|---|---| +| 继续运行 | `F5` | 运行到下一个断点或测试结束 | +| 逐过程 | `F10` | 执行当前行,不进入函数内部 | +| 逐语句 | `F11` | 执行当前行,并进入函数内部 | +| 跳出函数 | `Shift + F11` | 从当前函数返回调用处 | +| 停止调试 | `Shift + F5` | 结束调试 | + +如果想进入 `AreEqual()` 函数内部,应在调用处按 `F11`。 + +![单步进入被测函数](pictures/41.png) + +### 9.5 使用监视窗口查看变量 + +调试单元测试时,可以使用监视窗口查看变量值。 + +打开路径: + +```text +调试 -> 窗口 -> 监视 -> 监视 1 +``` + +可以输入: + +```text +ret +a +b +sum +``` + +通过监视窗口可以判断函数输入、输出和中间变量是否正确。 + +![监视窗口查看变量](pictures/42.png) + +### 9.6 使用调用堆栈查看调用关系 + +调用堆栈可以查看当前函数是被谁调用的。 + +打开路径: + +```text +调试 -> 窗口 -> 调用堆栈 +``` + +在单元测试调试中,调用堆栈通常可以看到: + +```text +被测函数 +测试方法 +测试框架入口 +``` + +通过调用堆栈可以确认程序是否从正确的测试方法进入了正确的被测函数。 + +![调用堆栈查看调用关系](pictures/43.png) + +### 9.7 调试异常测试 + +对于异常测试,可以在异常抛出位置设置断点。 + +例如: + +```cpp +int Div(int a, int b) +{ + if (b == 0) + { + throw std::invalid_argument("divisor can not be zero"); + } + + return a / b; +} +``` + +在 `throw` 语句处设置断点,然后调试测试: + +```cpp +TEST_METHOD(Div_DivisorIsZero_ThrowException) +{ + Assert::ExpectException([] { + Div(10, 0); + }); +} +``` + +程序停在 `throw` 处,说明异常路径确实被执行。 + +![调试异常测试](pictures/44.png) + +## 10. 常见问题和排查方法 + +### 10.1 测试资源管理器中看不到测试 + +可能原因: + +1. 测试项目没有生成成功; +2. 测试类没有使用 `TEST_CLASS`; +3. 测试方法没有使用 `TEST_METHOD`; +4. 测试文件没有加入测试项目; +5. 当前平台或配置不正确; +6. VS 测试适配器没有正常加载。 + +排查方法: + +1. 先重新生成解决方案; +2. 检查错误列表; +3. 检查测试代码结构; +4. 关闭并重新打开测试资源管理器; +5. 重启 VS2019。 + +### 10.2 找不到被测头文件 + +常见报错: + +```text +无法打开包括文件: “Calculator.h”: No such file or directory +``` + +原因通常是测试项目没有配置被测项目的头文件路径。 + +解决方法: + +```text +测试项目属性 -> 配置属性 -> C/C++ -> 常规 -> 附加包含目录 +``` + +添加被测项目头文件所在目录。 + +### 10.3 无法解析外部符号 + +常见报错: + +```text +error LNK2019: 无法解析的外部符号 +``` + +原因通常是测试项目找到了头文件,但没有链接被测项目生成的 `.lib` 文件。 + +解决方法: + +```text +测试项目属性 -> 配置属性 -> 链接器 -> 常规 -> 附加库目录 +测试项目属性 -> 配置属性 -> 链接器 -> 输入 -> 附加依赖项 +``` + +分别配置库文件路径和库文件名。 + +### 10.4 测试失败但不知道原因 + +测试失败时,应查看测试资源管理器中的详细信息。 + +重点查看: + +1. 哪个测试失败; +2. 失败的断言是哪一行; +3. 期望值是什么; +4. 实际值是什么; +5. 是否有异常信息。 + +如果仍然不清楚原因,应右键失败测试,选择“调试”,进入断点分析。 + +### 10.5 找不到代码覆盖率菜单 + +如果 VS2019 中找不到代码覆盖率菜单,可能原因如下: + +1. 当前 VS2019 版本不支持该功能; +2. 没有安装相关测试组件; +3. 当前项目类型不支持; +4. 测试项目没有被正常识别; +5. 解决方案没有成功生成。 + +排查时可以先确认: + +```text +测试 -> 分析代码覆盖率 +``` + +是否存在该菜单。如果没有,需要检查 VS2019 版本和安装组件。 + +## 11. 单元测试学习练习 + +### 11.1 练习一:创建单元测试项目 + +练习要求: + +1. 创建一个解决方案; +2. 创建一个被测代码项目 `CalculatorLib`; +3. 创建一个测试项目 `CalculatorTest`; +4. 在被测项目中添加 `Add()` 函数; +5. 在测试项目中编写一个测试方法; +6. 运行测试并通过。 + +验收重点:能够独立创建测试项目,并在测试资源管理器中看到测试结果。 + +### 11.2 练习二:编写多组测试样例 + +练习要求: + +1. 给 `Add()`、`Sub()`、`Mul()`、`Div()` 编写测试; +2. 每个函数至少编写一个正常值测试; +3. 至少编写一个边界值测试; +4. 至少编写一个异常测试; +5. 所有测试能够运行通过。 + +验收重点:测试样例名称清晰,测试内容覆盖正常情况、边界情况和异常情况。 + +### 11.3 练习三:查看代码覆盖度 + +练习要求: + +1. 运行全部单元测试; +2. 执行代码覆盖率分析; +3. 查看代码覆盖率结果; +4. 找出未覆盖代码; +5. 根据未覆盖代码补充测试样例。 + +验收重点:能够解释覆盖度含义,并能根据覆盖率结果补充测试。 + +### 11.4 练习四:调试失败测试 + +练习要求: + +1. 故意修改一个函数,让测试失败; +2. 运行测试并查看失败信息; +3. 对失败测试进行调试; +4. 使用 `F11` 进入被测函数; +5. 使用监视窗口查看变量; +6. 修改代码后重新运行测试。 + +验收重点:能够通过调试定位测试失败原因。 + +## 12. 验收重点总结 + +VS2019 单元测试验收时,应重点掌握以下内容: + +| 分类 | 必须掌握内容 | +|---|---| +| 单元测试创建 | 创建被测项目、创建测试项目、配置测试项目 | +| 测试样例书写 | `TEST_CLASS`、`TEST_METHOD`、断言、异常测试 | +| 测试运行 | 测试资源管理器、运行全部测试、运行单个测试 | +| 代码覆盖度 | 运行覆盖率分析、查看覆盖率结果、根据结果补充测试 | +| 运行性能 | 查看测试运行时间、理解 Debug/Release 对性能的影响 | +| 单元测试调试 | 设置断点、调试所选测试、监视变量、查看调用堆栈 | +| 常见问题排查 | 找不到测试、找不到头文件、无法解析外部符号 | + + + +## 13. 快捷键和常用操作总结 + +| 功能 | 操作 | +|---|---| +| 生成解决方案 | `Ctrl + Shift + B` | +| 打开测试资源管理器 | `测试 -> 测试资源管理器` | +| 运行所有测试 | 测试资源管理器中点击“运行全部” | +| 运行单个测试 | 右键测试方法 -> 运行 | +| 调试单个测试 | 右键测试方法 -> 调试 | +| 设置断点 | `F9` | +| 开始/继续调试 | `F5` | +| 逐过程 | `F10` | +| 逐语句 | `F11` | +| 跳出函数 | `Shift + F11` | +| 停止调试 | `Shift + F5` | +| 打开监视窗口 | `调试 -> 窗口 -> 监视` | +| 打开调用堆栈 | `调试 -> 窗口 -> 调用堆栈` | + +## 14. 学习结论 + +通过本次学习,掌握了在 VS2019 平台下进行 C/C++ 单元测试的基本流程。单元测试不是简单地运行程序,而是通过测试框架对指定函数或模块进行自动化验证。 + +学习过程中需要重点理解以下几点: + +1. 单元测试项目和被测代码项目通常分开管理; +2. 测试项目需要能够访问被测代码的头文件和库文件; +3. 测试样例应覆盖正常值、边界值和异常情况; +4. 断言是判断测试是否通过的核心; +5. 测试资源管理器用于运行和查看测试结果; +6. 代码覆盖度用于发现没有被测试执行到的代码; +7. 运行性能可以帮助发现耗时较长的测试或低效代码; +8. 单元测试失败时,应使用断点、监视窗口和调用堆栈进行调试。 + +单元测试的核心价值是:让代码修改后能够快速验证原有功能是否仍然正确,从而提高代码质量和维护效率。 + + diff --git a/yuwenhao/UnitTest/pictures/15.png b/yuwenhao/UnitTest/pictures/15.png new file mode 100644 index 0000000..f57b735 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/15.png differ diff --git a/yuwenhao/UnitTest/pictures/19.png b/yuwenhao/UnitTest/pictures/19.png new file mode 100644 index 0000000..889cb61 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/19.png differ diff --git a/yuwenhao/UnitTest/pictures/20.png b/yuwenhao/UnitTest/pictures/20.png new file mode 100644 index 0000000..31fa039 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/20.png differ diff --git a/yuwenhao/UnitTest/pictures/21.png b/yuwenhao/UnitTest/pictures/21.png new file mode 100644 index 0000000..5ad8b2d Binary files /dev/null and b/yuwenhao/UnitTest/pictures/21.png differ diff --git a/yuwenhao/UnitTest/pictures/22.png b/yuwenhao/UnitTest/pictures/22.png new file mode 100644 index 0000000..da81669 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/22.png differ diff --git a/yuwenhao/UnitTest/pictures/23.png b/yuwenhao/UnitTest/pictures/23.png new file mode 100644 index 0000000..c9cb5b2 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/23.png differ diff --git a/yuwenhao/UnitTest/pictures/24.png b/yuwenhao/UnitTest/pictures/24.png new file mode 100644 index 0000000..0a56d63 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/24.png differ diff --git a/yuwenhao/UnitTest/pictures/26.png b/yuwenhao/UnitTest/pictures/26.png new file mode 100644 index 0000000..d495f26 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/26.png differ diff --git a/yuwenhao/UnitTest/pictures/27.png b/yuwenhao/UnitTest/pictures/27.png new file mode 100644 index 0000000..4b0f42e Binary files /dev/null and b/yuwenhao/UnitTest/pictures/27.png differ diff --git a/yuwenhao/UnitTest/pictures/28.png b/yuwenhao/UnitTest/pictures/28.png new file mode 100644 index 0000000..8c70257 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/28.png differ diff --git a/yuwenhao/UnitTest/pictures/3.png b/yuwenhao/UnitTest/pictures/3.png new file mode 100644 index 0000000..718016f Binary files /dev/null and b/yuwenhao/UnitTest/pictures/3.png differ diff --git a/yuwenhao/UnitTest/pictures/35.png b/yuwenhao/UnitTest/pictures/35.png new file mode 100644 index 0000000..ec5e654 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/35.png differ diff --git a/yuwenhao/UnitTest/pictures/36.png b/yuwenhao/UnitTest/pictures/36.png new file mode 100644 index 0000000..0c71d85 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/36.png differ diff --git a/yuwenhao/UnitTest/pictures/39.png b/yuwenhao/UnitTest/pictures/39.png new file mode 100644 index 0000000..f37750f Binary files /dev/null and b/yuwenhao/UnitTest/pictures/39.png differ diff --git a/yuwenhao/UnitTest/pictures/40.png b/yuwenhao/UnitTest/pictures/40.png new file mode 100644 index 0000000..e14c905 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/40.png differ diff --git a/yuwenhao/UnitTest/pictures/41.png b/yuwenhao/UnitTest/pictures/41.png new file mode 100644 index 0000000..a2a3186 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/41.png differ diff --git a/yuwenhao/UnitTest/pictures/42.png b/yuwenhao/UnitTest/pictures/42.png new file mode 100644 index 0000000..e97c8ea Binary files /dev/null and b/yuwenhao/UnitTest/pictures/42.png differ diff --git a/yuwenhao/UnitTest/pictures/43.png b/yuwenhao/UnitTest/pictures/43.png new file mode 100644 index 0000000..4620c6e Binary files /dev/null and b/yuwenhao/UnitTest/pictures/43.png differ diff --git a/yuwenhao/UnitTest/pictures/44.png b/yuwenhao/UnitTest/pictures/44.png new file mode 100644 index 0000000..b5d5daf Binary files /dev/null and b/yuwenhao/UnitTest/pictures/44.png differ diff --git a/yuwenhao/UnitTest/pictures/6.png b/yuwenhao/UnitTest/pictures/6.png new file mode 100644 index 0000000..3451070 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/6.png differ diff --git a/yuwenhao/UnitTest/pictures/7.png b/yuwenhao/UnitTest/pictures/7.png new file mode 100644 index 0000000..9efaf36 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/7.png differ diff --git a/yuwenhao/UnitTest/pictures/8.png b/yuwenhao/UnitTest/pictures/8.png new file mode 100644 index 0000000..3d8046c Binary files /dev/null and b/yuwenhao/UnitTest/pictures/8.png differ diff --git a/yuwenhao/UnitTest/pictures/9.png b/yuwenhao/UnitTest/pictures/9.png new file mode 100644 index 0000000..69cc377 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/9.png differ diff --git a/yuwenhao/UnitTest/pictures/代码覆盖率.png b/yuwenhao/UnitTest/pictures/代码覆盖率.png new file mode 100644 index 0000000..2e880a9 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/代码覆盖率.png differ diff --git a/yuwenhao/UnitTest/pictures/覆盖率结果.png b/yuwenhao/UnitTest/pictures/覆盖率结果.png new file mode 100644 index 0000000..5020269 Binary files /dev/null and b/yuwenhao/UnitTest/pictures/覆盖率结果.png differ