cpp-unit-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unit Testing

单元测试

1. Benefits

1. 优势

  • Readability
    Ensures high code quality and reliability. Tests are self-documenting, reducing cognitive load for reviewers and maintainers.
  • Consistency
    Uniform structure across tests ensures predictable, familiar code that team members can navigate efficiently.
  • Scalability
    Table-driven and data-driven approaches minimize boilerplate code when adding new test cases, making it simple to expand coverage.
  • Debuggability
    Scoped traces and detailed assertion messages pinpoint failures quickly during continuous integration and local testing.
  • 可读性
    确保高代码质量和可靠性。测试具备自文档性,降低评审人员和维护人员的认知负担。
  • 一致性
    所有测试采用统一结构,形成可预测、易熟悉的代码,团队成员可高效浏览。
  • 可扩展性
    表格驱动和数据驱动方法在添加新测试用例时最大程度减少样板代码,轻松扩展测试覆盖率。
  • 可调试性
    范围跟踪和详细断言信息可在持续集成和本地测试期间快速定位故障。

2. Principles

2. 原则

2.1. FIRST

2.1. FIRST

The
FIRST
principles for unit testing focus on creating effective and maintainable tests.
  • Fast
    Unit tests should execute quickly to provide rapid feedback during development and continuous integration.
  • Independent
    Each unit test should be self-contained and not rely on the state or behavior of other tests.
  • Repeatable
    Unit tests should produce deterministic results every time they are run, regardless of the environment or order of execution.
  • Self-Validating
    Unit tests should have clear pass/fail outcomes without requiring manual inspection.
  • Timely
    Unit tests should be written and executed early in the development process to catch issues as soon as possible.
单元测试的
FIRST
原则专注于创建高效且可维护的测试。
  • 快速(Fast)
    单元测试应快速执行,以便在开发和持续集成过程中提供快速反馈。
  • 独立(Independent)
    每个单元测试应独立存在,不依赖其他测试的状态或行为。
  • 可重复(Repeatable)
    无论执行环境或顺序如何,单元测试每次运行都应产生确定的结果。
  • 自验证(Self-Validating)
    单元测试应具备明确的通过/失败结果,无需人工检查。
  • 及时(Timely)
    单元测试应在开发流程早期编写和执行,以便尽早发现问题。

3. Patterns

3. 模式

3.1. In-Got-Want

3.1. In-Got-Want

The In-Got-Want pattern structures each test case into three clear sections.
  • In
    Defines the input parameters or conditions for the test.
  • Got
    Captures the actual output or result produced by the code under test.
  • Want
    Specifies the expected output or result that the test is verifying against.
In-Got-Want模式将每个测试用例划分为三个清晰的部分。
  • In(输入)
    定义测试的输入参数或条件。
  • Got(实际输出)
    捕获被测代码产生的实际输出或结果。
  • Want(预期输出)
    指定测试要验证的预期输出或结果。

3.2. Table-Driven Testing

3.2. 表格驱动测试

Table-driven testing organizes test cases in a tabular format, allowing multiple scenarios to be defined concisely.
  • Test Case Structure
    Each row in the table represents a distinct test case with its own set of inputs and expected outputs.
  • Iteration
    The test framework iterates over each row, executing the same test logic with different data.
表格驱动测试以表格形式组织测试用例,可简洁定义多个场景。
  • 测试用例结构
    表格中的每一行代表一个独立的测试用例,包含自身的输入和预期输出。
  • 迭代执行
    测试框架遍历每一行,使用不同数据执行相同的测试逻辑。

3.3. Data-Driven Testing (DDT)

3.3. 数据驱动测试(DDT)

Data-driven testing separates test data from test logic, enabling the same test logic to be executed with multiple sets of input data.
  • External Data Sources
    Test data can be stored in external files (e.g., JSON, CSV) and loaded at runtime.
  • Reusability
    The same test logic can be reused with different datasets, enhancing maintainability and coverage.
数据驱动测试将测试数据与测试逻辑分离,使相同的测试逻辑可与多组输入数据一起执行。
  • 外部数据源
    测试数据可存储在外部文件(如JSON、CSV)中,并在运行时加载。
  • 可复用性
    相同的测试逻辑可与不同数据集复用,提升可维护性和测试覆盖率。

3.4. Arrange, Act, Assert (AAA)

3.4. 准备-执行-断言(AAA)

The AAA pattern structures each test case into three clear phases.
  • Arrange
    Set up the necessary preconditions and inputs for the test.
  • Act
    Execute the function or method being tested.
  • Assert
    Verify that the actual output matches the expected output.
AAA模式将每个测试用例划分为三个清晰的阶段。
  • 准备(Arrange)
    设置测试所需的必要前置条件和输入。
  • 执行(Act)
    执行被测函数或方法。
  • 断言(Assert)
    验证实际输出是否与预期输出匹配。

3.5. Test Fixtures

3.5. 测试夹具

Test fixtures provide a consistent and reusable setup and teardown mechanism for test cases.
  • Setup
    Initialize common objects or state needed for multiple tests.
  • Teardown
    Clean up resources or reset state after each test.
测试夹具为测试用例提供一致且可复用的初始化和清理机制。
  • 初始化(Setup)
    初始化多个测试所需的通用对象或状态。
  • 清理(Teardown)
    在每个测试后清理资源或重置状态。

3.6. Test Doubles

3.6. 测试替身

Test doubles (e.g., mocks, stubs, fakes) are simplified versions of complex objects or components used to isolate the unit under test.
  • Mocks
    Simulate the behavior of real objects and verify interactions.
  • Stubs
    Provide predefined responses to method calls without implementing full behavior.
  • Fakes
    Implement simplified versions of real objects with limited functionality.
测试替身(如模拟对象、桩对象、伪对象)是复杂对象或组件的简化版本,用于隔离被测单元。
  • 模拟对象(Mocks)
    模拟真实对象的行为并验证交互。
  • 桩对象(Stubs)
    为方法调用提供预定义响应,无需实现完整行为。
  • 伪对象(Fakes)
    实现真实对象的简化版本,仅具备有限功能。

4. Workflow

4. 工作流程

  1. Identify
    Identify new functions in
    src/
    (e.g.,
    src/<module>/<header>.hpp
    ).
  2. Add/Create
    Create new tests colocated with source code in
    src/<module>/
    (e.g.,
    src/<module>/<header>_test.cpp
    ).
  3. Register with CMake
    Add the test file to
    src/<module>/CMakeLists.txt
    using
    meta_gtest()
    with appropriate options (e.g.,
    WITH_DDT
    ).
    The test configuration should use
    ENABLE
    option with
    META_BUILD_TESTING
    variable:
    cmake
    include(meta_gtest)
    
    meta_gtest(
        ENABLE ${META_BUILD_TESTING}
        TARGET ${PROJECT_NAME}-test
        SOURCES
            <header>_test.cpp
        LINK
            ${PROJECT_NAME}::<module>
    )
  4. Test Coverage Requirements
    Include comprehensive edge cases:
    • Coverage-guided cases
    • Boundary values (min/max limits, edge thresholds)
    • Empty/null inputs
    • Null pointers and invalid references
    • Overflow/underflow scenarios
    • Special cases (negative numbers, zero, special states)
  5. Apply Templates
    Structure all tests using the template pattern below.
  1. 识别
    识别
    src/
    中的新函数(如
    src/<module>/<header>.hpp
    )。
  2. 添加/创建
    在源代码所在目录
    src/<module>/
    中创建新测试文件(如
    src/<module>/<header>_test.cpp
    )。
  3. 向CMake注册
    使用
    meta_gtest()
    并添加适当选项(如
    WITH_DDT
    ),将测试文件添加到
    src/<module>/CMakeLists.txt
    中。
    测试配置应使用
    ENABLE
    选项并结合
    META_BUILD_TESTING
    变量:
    cmake
    include(meta_gtest)
    
    meta_gtest(
        ENABLE ${META_BUILD_TESTING}
        TARGET ${PROJECT_NAME}-test
        SOURCES
            <header>_test.cpp
        LINK
            ${PROJECT_NAME}::<module>
    )
  4. 测试覆盖率要求
    需涵盖全面的边缘场景:
    • 覆盖率引导的测试用例
    • 边界值(最小/最大限制、边缘阈值)
    • 空/空值输入
    • 空指针和无效引用
    • 溢出/下溢场景
    • 特殊情况(负数、零、特殊状态)
  5. 应用模板
    使用以下模板结构编写所有测试。

5. Commands

5. 命令

CommandDescription
make cmake-gcc-test-unit-build
CMake preset configuration and Compile with Ninja
make cmake-gcc-test-unit-run
Execute tests via ctest
make cmake-gcc-test-unit-coverage
Execute tests via ctest and generate coverage reports
命令描述
make cmake-gcc-test-unit-build
CMake预设配置并使用Ninja编译
make cmake-gcc-test-unit-run
通过ctest执行测试
make cmake-gcc-test-unit-coverage
通过ctest执行测试并生成覆盖率报告

6. Style Guide

6. 风格指南

  • Test Framework
    Use GoogleTest (GTest) framework via
    #include <gtest/gtest.h>
    .
  • Include Headers
    Include necessary standard library headers (
    <vector>
    ,
    <string>
    ,
    <climits>
    , etc.) and module-specific headers in a logical order: system headers first, then project headers.
    Include necessary headers in this order:
    1. GTest/GMock headers (
      <gtest/gtest.h>
      ,
      <gmock/gmock.h>
      )
    2. Standard library headers (
      <memory>
      ,
      <string>
      , etc.)
    3. Project interface headers
    4. Project implementation headers
  • Namespace
    Use
    using namespace <namespace>;
    for convenience within test functions to reduce verbosity while maintaining clarity, since test scope is limited.
  • Test Organization
    Consolidate test cases for a single function into one
    TEST(...)
    function
    using table-driven testing.
    This approach:
    • Eliminates redundant test function definitions
    • Simplifies maintenance by grouping related scenarios together
    • Reduces code duplication in setup and teardown phases
    • Makes it easier to add or modify test cases
  • Testing Macros
    Focus each
    TEST(...)
    function on a single function or cohesive behavior. For complex setups, use
    TEST_F
    fixtures or helper functions to reduce duplication.
  • Mocking
    Use Google Mock (GMock) for creating test doubles (mocks, stubs, fakes) to isolate the unit under test. See the cpp-mock-testing skill.
  • Traceability
    Employ
    SCOPED_TRACE(tc.label)
    for traceable failures in table-driven tests.
  • Assertions
    Use
    EXPECT_*
    macros (not
    ASSERT_*
    ) to allow all test cases to run.
  • 测试框架
    通过
    #include <gtest/gtest.h>
    使用GoogleTest (GTest)框架。
  • 头文件包含
    按逻辑顺序包含必要的标准库头文件(
    <vector>
    <string>
    <climits>
    等)和模块特定头文件:先包含系统头文件,再包含项目头文件。
    按以下顺序包含必要的头文件:
    1. GTest/GMock头文件(
      <gtest/gtest.h>
      <gmock/gmock.h>
    2. 标准库头文件(
      <memory>
      <string>
      等)
    3. 项目接口头文件
    4. 项目实现头文件
  • 命名空间
    在测试函数中使用
    using namespace <namespace>;
    以简化代码,同时保持清晰性,因为测试范围有限。
  • 测试组织
    使用表格驱动测试,将单个函数的所有测试用例整合到一个
    TEST(...)
    函数
    中。
    这种方法:
    • 消除冗余的测试函数定义
    • 通过分组相关场景简化维护
    • 减少初始化和清理阶段的代码重复
    • 更轻松地添加或修改测试用例
  • 测试宏
    每个
    TEST(...)
    函数专注于单个函数或内聚行为。对于复杂的初始化,使用
    TEST_F
    夹具或辅助函数减少重复代码。
  • 模拟
    使用Google Mock (GMock)创建测试替身(模拟对象、桩对象、伪对象)以隔离被测单元。请参阅cpp-mock-testing技能文档。
  • 可追溯性
    在表格驱动测试中使用
    SCOPED_TRACE(tc.label)
    实现可追溯的故障定位。
  • 断言
    使用
    EXPECT_*
    宏(而非
    ASSERT_*
    )以允许所有测试用例执行完毕。

7. Template

7. 模板

Use these templates for new unit tests. Replace placeholders with actual values.
使用以下模板创建新的单元测试。将占位符替换为实际值。

7.1. File Header Template

7.1. 文件头模板

cpp
#include <gtest/gtest.h>

#include <string>
#include <vector>

#include "<module>/<header>.hpp"

using namespace <namespace>;
cpp
#include <gtest/gtest.h>

#include <string>
#include <vector>

#include "<module>/<header>.hpp"

using namespace <namespace>;

7.2. Table-Driven Test Template

7.2. 表格驱动测试模板

cpp
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>);
  }
}
cpp
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. Test Fixture Template

7.3. 测试夹具模板

cpp
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>);
}
cpp
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. Exception Test Template

7.4. 异常测试模板

cpp
TEST(<Module>Test, <FunctionName>ThrowsOnInvalidInput)
{
  // Arrange
  <Module> object;
  auto invalid_input = <invalid_value>;

  // Act & Assert
  EXPECT_THROW(object.<function>(invalid_input), <ExceptionType>);
}
cpp
TEST(<Module>Test, <FunctionName>ThrowsOnInvalidInput)
{
  // Arrange
  <Module> object;
  auto invalid_input = <invalid_value>;

  // Act & Assert
  EXPECT_THROW(object.<function>(invalid_input), <ExceptionType>);
}

7.5. Boundary Value Test Template

7.5. 边界值测试模板

cpp
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);
  }
}
cpp
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. Data-Driven Test Template (JSON)

7.6. 数据驱动测试模板(JSON)

cpp
#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);
  }
}
cpp
#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);
  }
}

8. References

8. 参考资料