关于 cpp-unit-testing
面向 AI 编码 Agent 的 C++ 单元测试技能,提供统一的测试模式、工作流程和模板,帮助在项目中创建及运行测试。
主要功能
- 01FIRST 测试原则
- 02In-Got-Want 与 AAA 模式
- 03表驱动和数据驱动测试
- 04测试夹具与测试替身
- 05CMake 测试注册流程
- 06常见测试用例模板
适用场景
- 01为新的 C++ 函数编写测试
- 02补充边界值和无效输入用例
- 03在团队中统一测试结构
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sentenz/ skills --skill cpp-unit-testing安装后在 Claude Code 中输入/cpp-unit-testing确认已加载。
详细介绍
单元测试
为 AI 编码 Agent 提供的说明,指导如何使用一致的软件测试模式,在此 C++ 项目中自动创建单元测试。
1. 优点
-
可读性
确保高质量和可靠的代码。测试具有自我说明性,可减轻审查者和维护者的认知负担。
-
一致性
统一的测试结构让代码更可预测、更熟悉,团队成员可以高效地浏览。
-
可扩展性
表驱动和数据驱动方法在添加新测试用例时可最大限度减少样板代码,让扩展覆盖范围更加简单。
-
易于调试
作用域限定的跟踪信息和详细的断言消息,有助于在持续集成和本地测试期间快速定位故障。
2. 原则
2.1. FIRST
单元测试的 FIRST 原则旨在创建有效且易于维护的测试。
-
快速
单元测试应快速执行,以便在开发和持续集成期间及时提供反馈。
-
独立
每个单元测试都应自成一体,不依赖其他测试的状态或行为。
-
可重复
无论环境或执行顺序如何,单元测试每次运行都应产生确定的结果。
-
自验证
单元测试应明确给出通过或失败的结果,无需人工检查。
-
及时
应在开发过程早期编写并运行单元测试,以尽早发现问题。
3. 模式
3.1. In-Got-Want
In-Got-Want 模式将每个测试用例划分为三个清晰的部分。
-
In
定义测试的输入参数或条件。
-
Got
记录被测代码产生的实际输出或结果。
-
Want
指定测试要验证的预期输出或结果。
3.2. 表驱动测试
表驱动测试以表格形式组织测试用例,从而简洁地定义多个场景。
-
测试用例结构
表格中的每一行代表一个独立的测试用例,其中包含各自的输入和预期输出。
-
迭代
测试框架遍历每一行,使用不同的数据执行相同的测试逻辑。
3.3. 数据驱动测试(DDT)
数据驱动测试将测试数据与测试逻辑分离,使相同的测试逻辑能够使用多组输入数据执行。
-
外部数据源
测试数据可以存储在外部文件(例如 JSON、CSV)中,并在运行时加载。
-
可复用性
相同的测试逻辑可以与不同的数据集重复使用,从而提升可维护性和覆盖范围。
3.4. 准备、执行、断言(AAA)
AAA 模式将每个测试用例划分为三个清晰的阶段。
-
准备
设置测试所需的前置条件和输入。
-
执行
执行被测函数或方法。
-
断言
验证实际输出是否符合预期输出。
3.5. 测试夹具
测试夹具为测试用例提供一致且可复用的设置和清理机制。
-
设置
初始化多个测试所需的通用对象或状态。
-
清理
在每次测试后清理资源或重置状态。
3.6. 测试替身
测试替身(例如,模拟对象、桩对象、伪对象)是复杂对象或组件的简化版本,用于隔离被测单元。
-
模拟对象
模拟真实对象的行为并验证交互。
-
桩对象
为方法调用提供预定义响应,而不实现完整行为。
-
伪对象
实现功能有限的真实对象的简化版本。
4. 工作流
-
识别
识别
src/中的新函数(例如,src/<module>/<header>.hpp)。 -
添加/创建
在源代码所在目录
src/<module>/中创建新测试(例如,src/<module>/<header>_test.cpp)。 -
在 CMake 中注册
使用
meta_gtest()和适当的选项(例如,WITH_DDT)将测试文件添加到src/<module>/CMakeLists.txt。测试配置应使用
ENABLE选项和META_BUILD_TESTING变量:include(meta_gtest) meta_gtest( ENABLE ${META_BUILD_TESTING} TARGET ${PROJECT_NAME}-test SOURCES <header>_test.cpp LINK ${PROJECT_NAME}::<module> ) -
测试覆盖率要求
包含全面的边界情况:
- 覆盖率引导的用例
- 边界值(最小/最大限制、边界阈值)
- 空/空值输入
- 空指针和无效引用
- 溢出/下溢场景
- 特殊情况(负数、零、特殊状态)
-
应用模板
按照以下模板模式组织所有测试。
5. 命令
| 命令 | 描述 |
|---|---|
make cmake-gcc-test-unit-build | 配置 CMake 预设并使用 Ninja 编译 |
make cmake-gcc-test-unit-run | 通过 ctest 执行测试 |
make cmake-gcc-test-unit-coverage | 通过 ctest 执行测试并生成覆盖率报告 |
6. 风格指南
-
测试框架
通过
#include <gtest/gtest.h>使用 GoogleTest (GTest) 框架。 -
包含头文件
按照合理的顺序包含必要的标准库头文件(
<vector>、<string>、<climits>等)和模块专用头文件:先包含系统头文件,再包含项目头文件。按以下顺序包含必要的头文件:
- GTest/GMock 头文件(
<gtest/gtest.h>、<gmock/gmock.h>) - 标准库头文件(
<memory>、<string>等) - 项目接口头文件
- 项目实现头文件
- GTest/GMock 头文件(
-
命名空间
在测试函数中使用
using namespace <namespace>;来简化代码,同时保持清晰,因为测试的作用范围有限。 -
测试组织
使用表驱动测试,将同一函数的测试用例整合到一个
TEST(...)函数中。这种方法可以:
- 避免重复定义测试函数
- 将相关场景分组,简化维护
- 减少设置和清理阶段的代码重复
- 更便于添加或修改测试用例
-
测试宏
每个
TEST(...)函数应专注于单个函数或一组紧密相关的行为。对于复杂的设置,可使用TEST_F测试夹具或辅助函数来减少重复代码。 -
模拟
使用 Google Mock (GMock) 创建测试替身(模拟对象、桩对象、伪对象),以隔离被测单元。参见 cpp-mock-testing 技能。
-
可追溯性
在表驱动测试中使用
SCOPED_TRACE(tc.label),以便追踪失败原因。 -
断言
使用
EXPECT_*宏(而非ASSERT_*),以便运行所有测试用例。
7. 模板
使用以下模板编写新的单元测试。将占位符替换为实际值。
7.1. 文件头模板
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include "<module>/<header>.hpp"
using namespace <namespace>;
7.2. 表驱动测试模板
TEST(<Module>Test, <FunctionName>)
{
// In-Got-Want
struct Tests
{
std::string label;
struct In
{
/* input types and names */
} in;
struct Want
{
/* expected output type(s) and name(s) */
} want;
};
// Table-Driven Testing
const std::vector<Tests> tests = {
{"case-description-1", {/* input */}, {/* expected */}},
{"case-description-2", {/* input */}, {/* expected */}},
};
for (const auto &tc : tests)
{
SCOPED_TRACE(tc.label);
// Arrange
<Module> <object>;
// Act
auto got = <object>.<function>(tc.in.<input>);
// Assert
EXPECT_EQ(got, tc.want.<expected>);
}
}
7.3. 测试夹具模板
class <Module>Test : public ::testing::Test
{
protected:
void SetUp() override
{
// Initialize common objects or state
}
void TearDown() override
{
// Clean up resources or reset state
}
<Module> object_;
};
TEST_F(<Module>Test, <FunctionName>)
{
// Arrange
auto input = <input_value>;
// Act
auto got = object_.<function>(input);
// Assert
EXPECT_EQ(got, <expected>);
}
7.4. 异常测试模板
TEST(<Module>Test, <FunctionName>ThrowsOnInvalidInput)
{
// Arrange
<Module> object;
auto invalid_input = <invalid_value>;
// Act & Assert
EXPECT_THROW(object.<function>(invalid_input), <ExceptionType>);
}
7.5. 边界值测试模板
TEST(<Module>Test, <FunctionName>BoundaryValues)
{
// In-Got-Want
struct Tests
{
std::string label;
struct In
{
<input_type> input;
} in;
struct Want
{
<output_type> expected;
} want;
};
// Table-Driven Testing with boundary cases
const std::vector<Tests> tests = {
{"minimum-value", {<MIN_VALUE>}, {/* expected */}},
{"maximum-value", {<MAX_VALUE>}, {/* expected */}},
{"zero-value", {0}, {/* expected */}},
{"empty-input", {{}}, {/* expected */}},
{"negative-value", {-1}, {/* expected */}},
};
for (const auto &tc : tests)
{
SCOPED_TRACE(tc.label);
// Arrange
<Module> object;
// Act
auto got = object.<function>(tc.in.input);
// Assert
EXPECT_EQ(got, tc.want.expected);
}
}
7.6. 数据驱动测试模板(JSON)
#include <nlohmann/json.hpp>
#include <fstream>
TEST(<Module>Test, <FunctionName>DataDriven)
{
// Load test data from JSON file
std::ifstream file("<module>/<header>_test.json");
nlohmann::json test_data;
file >> test_data;
for (const auto &tc : test_data["tests"])
{
SCOPED_TRACE(tc["label"].get<std::string>());
// Arrange
<Module> object;
auto input = tc["in"]["input"].get<<input_type>>();
auto expected = tc["want"]["expected"].get<<output_type>>();
// Act
auto got = object.<function>(input);
// Assert
EXPECT_EQ(got, expected);
}
}