C++ 缺乏内置的单元测试支持,但社区已经发展出多个成熟、功能丰富且各具特色的测试框架。Google Test 凭借其庞大的生态和 Google Mock 的集成,成为企业级项目的默认选择;Catch2 以单头文件、BDD 风格和自然语言断言著称;Doctest 则追求极致轻量和编译速度。本文将系统对比这三种框架的核心能力,并深入讲解 Mock 技巧和测试覆盖率集成——这是 https://plumephp.com/cpp-engineering-practices/ 中提到的代码质量保障核心环节。
一、Google Test 框架
1.1 基本断言宏
Google Test 提供两类断言:ASSERT_*(致命断言,失败后立即终止当前测试)和 EXPECT_*(非致命断言,失败继续执行)。
#include <gtest/gtest.h>
int add(int a, int b) { return a + b; }
TEST(MathTest, AddPositiveNumbers) {
EXPECT_EQ(add(2, 3), 5); // 非致命:失败继续
ASSERT_EQ(add(0, 0), 0); // 致命:失败立即退出
EXPECT_GT(add(-1, 3), 0); // 大于
EXPECT_LT(add(1, 2), 10); // 小于
EXPECT_NE(add(1, 1), 3); // 不等于
EXPECT_DOUBLE_EQ(0.1 + 0.2, 0.3); // 浮点相等(容差)
}
1.2 测试夹具(Test Fixture)
当多个测试共享相同的初始化和清理逻辑时,使用 TEST_F 和夹具类:
class DatabaseTest : public ::testing::Test {
protected:
void SetUp() override {
db_ = create_test_database();
db_->connect("localhost", "test_db");
db_->clear_tables();
}
void TearDown() override {
db_->disconnect();
}
std::unique_ptr<Database> db_;
};
TEST_F(DatabaseTest, InsertUser) {
User user{"alice", 25};
EXPECT_TRUE(db_->insert(user));
auto found = db_->find("alice");
EXPECT_TRUE(found.has_value());
EXPECT_EQ(found->age, 25);
}
TEST_F(DatabaseTest, DeleteUser) {
db_->insert(User{"bob", 30});
EXPECT_TRUE(db_->remove("bob"));
EXPECT_FALSE(db_->find("bob").has_value());
}
每个 TEST_F 都会独立实例化一个 DatabaseTest 对象,SetUp / TearDown 保证测试隔离。
1.3 参数化测试
当同一套测试逻辑需要应用于多组输入时,使用参数化测试:
class FibonacciTest : public ::testing::TestWithParam<std::pair<int, int>> {};
TEST_P(FibonacciTest, ComputesCorrectly) {
auto [input, expected] = GetParam();
EXPECT_EQ(fibonacci(input), expected);
}
INSTANTIATE_TEST_SUITE_P(
FibonacciValues,
FibonacciTest,
::testing::Values(
std::make_pair(0, 0),
std::make_pair(1, 1),
std::make_pair(10, 55),
std::make_pair(20, 6765)
)
);
1.4 类型参数化测试
模板函数可以用类型参数化测试覆盖多个类型实例:
template <typename T>
class ContainerTest : public ::testing::Test {};
using MyTypes = ::testing::Types<std::vector<int>, std::list<int>, std::deque<int>>;
TYPED_TEST_SUITE(ContainerTest, MyTypes);
TYPED_TEST(ContainerTest, PushBack) {
TypeParam container;
container.push_back(42);
EXPECT_EQ(container.size(), 1u);
EXPECT_EQ(container.back(), 42);
}
二、Google Mock 模拟对象
在单元测试中,被测模块往往依赖尚未实现的外部组件(数据库、网络服务、硬件接口)。Google Mock 允许创建模拟对象,预先设定行为期望,从而隔离被测单元。
2.1 基本 Mock 用法
#include <gmock/gmock.h>
#include <gtest/gtest.h>
class PaymentGateway {
public:
virtual ~PaymentGateway() = default;
virtual bool charge(int amount, const std::string& card_id) = 0;
virtual void refund(const std::string& transaction_id) = 0;
virtual int get_balance(const std::string& account) = 0;
};
class MockPaymentGateway : public PaymentGateway {
public:
MOCK_METHOD(bool, charge, (int amount, const std::string& card_id), (override));
MOCK_METHOD(void, refund, (const std::string& transaction_id), (override));
MOCK_METHOD(int, get_balance, (const std::string& account), (override));
};
2.2 设定期望
class OrderService {
PaymentGateway* gateway_;
public:
explicit OrderService(PaymentGateway* gw) : gateway_(gw) {}
bool process_order(int amount, const std::string& card) {
if (gateway_->charge(amount, card)) {
return true;
}
return false;
}
};
using ::testing::_;
using ::testing::Return;
using ::testing::AtLeast;
using ::testing::Throw;
TEST(OrderServiceTest, SuccessfulCharge) {
MockPaymentGateway mock;
OrderService service(&mock);
// 期望:charge(100, "card_123") 被调用一次,返回 true
EXPECT_CALL(mock, charge(100, "card_123"))
.Times(1)
.WillOnce(Return(true));
EXPECT_TRUE(service.process_order(100, "card_123"));
}
TEST(OrderServiceTest, FailedCharge) {
MockPaymentGateway mock;
OrderService service(&mock);
// 使用通配符匹配任何参数
EXPECT_CALL(mock, charge(_, _))
.WillOnce(Return(false));
EXPECT_FALSE(service.process_order(200, "invalid_card"));
}
2.3 匹配器与行为序列
using ::testing::Ge;
using ::testing::StartsWith;
using ::testing::InSequence;
TEST(MockAdvancedTest, SequenceAndMatchers) {
MockPaymentGateway mock;
// 参数匹配器
EXPECT_CALL(mock, charge(Ge(100), StartsWith("visa_")))
.WillRepeatedly(Return(true));
// 行为序列
{
InSequence seq;
EXPECT_CALL(mock, charge(50, _)).WillOnce(Return(true));
EXPECT_CALL(mock, refund(_)).Times(1);
}
// 抛出异常
EXPECT_CALL(mock, get_balance("blocked"))
.WillOnce(Throw(std::runtime_error("account blocked")));
}
三、Catch2:BDD 风格的轻量选择
Catch2 以单头文件分发、无需链接、自然语言断言闻名。它非常适合快速原型和小型项目。
3.1 安装与基本使用
// 下载 catch_amalgamated.hpp / .cpp 或 vcpkg install catch2
#include <catch2/catch_test_macros.hpp>
int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
TEST_CASE("Factorial computation", "[math]") {
REQUIRE(factorial(0) == 1);
REQUIRE(factorial(1) == 1);
REQUIRE(factorial(5) == 120);
}
3.2 BDD 风格
Catch2 原生支持 Given-When-Then 风格,使测试用例读起来像需求规格:
SCENARIO("User account login", "[auth]") {
GIVEN("A registered user with valid credentials") {
UserAccount account("alice", "secret123");
WHEN("the user enters correct password") {
auto result = account.authenticate("secret123");
THEN("login succeeds") {
REQUIRE(result == AuthResult::Success);
}
}
WHEN("the user enters wrong password") {
auto result = account.authenticate("wrong");
THEN("login fails") {
REQUIRE(result == AuthResult::InvalidPassword);
}
}
}
}
3.3 分段(SECTION)
Catch2 的 SECTION 允许在同一个 TEST_CASE 中共享 setup 代码但执行不同分支:
TEST_CASE("Vector operations", "[container]") {
std::vector<int> vec{1, 2, 3};
SECTION("push_back") {
vec.push_back(4);
REQUIRE(vec.size() == 4);
}
SECTION("pop_back") {
vec.pop_back();
REQUIRE(vec.size() == 2);
}
// 每个 SECTION 执行前 vec 都是 {1, 2, 3}
}
四、Doctest:编译速度的极致
Doctest 的设计哲学是**「像使用 assert 一样轻量」**。它的头文件比 Catch2 更小,编译时间更短,且可以直接把测试写在生产代码旁边。
// 直接在实现文件中写测试
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
int add(int a, int b) { return a + b; }
TEST_CASE("basic addition") {
CHECK(add(2, 2) == 4);
CHECK(add(-1, 1) == 0);
}
Doctest 也支持子 case 和模板测试:
TEST_CASE_TEMPLATE("container size", T, std::vector<int>, std::list<int>) {
T container;
CHECK(container.size() == 0);
container.push_back(1);
CHECK(container.size() == 1);
}
五、测试覆盖率:gcov 与 lcov
光有测试不够,还需要知道代码的测试覆盖程度。GCC/Clang 内置 gcov 覆盖率采集支持,lcov 负责生成 HTML 报告。
5.1 编译带覆盖率的代码
g++ -O0 -g --coverage -fprofile-arcs -ftest-coverage \
test_example.cpp -o test_example
./test_example # 生成 .gcda 文件
5.2 生成 HTML 报告
# 采集数据
gcov test_example.cpp
# 生成 lcov 信息文件
lcov --capture --directory . --output-file coverage.info
# 过滤系统头文件
lcov --remove coverage.info '/usr/*' --output-file coverage.info
# 生成 HTML
genhtml coverage.info --output-directory coverage_report
5.3 CMake 集成
option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
if(ENABLE_COVERAGE)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
add_compile_options(--coverage -O0)
add_link_options(--coverage)
endif()
endif()
enable_testing()
add_executable(my_tests test_math.cpp)
target_link_libraries(my_tests gtest_main)
gtest_discover_tests(my_tests)
运行:
cmake -S . -B build -DENABLE_COVERAGE=ON
cmake --build build
cd build && ctest
gcov src/*.cpp
5.4 覆盖率指标解读
- 行覆盖率:至少执行过一次的代码行比例;
- 分支覆盖率:if、switch、?: 运算符的每个分支是否都被覆盖,比行覆盖率更严格;
- 函数覆盖率:每个函数是否被至少调用一次。
目标:核心业务逻辑的行覆盖率应达到 80%+,安全关键模块应达到 90%+。但覆盖率数字不应成为唯一目标——100% 覆盖率不等于没有 bug,它只是确保代码至少被触达过。
相关阅读
- https://plumephp.com/cpp-engineering-practices/ — C++ 代码规范、测试策略与 CI/CD 集成全景
- https://plumephp.com/cpp-cmake-project/ — CMake 中集成第三方测试框架(FetchContent/vcpkg 引入 GTest)
- https://plumephp.com/cpp-cross-platform-build-matrix/ — CI 矩阵中多编译器测试覆盖的实践
延伸阅读
- https://plumephp.com/posts/testing/ — 软件测试理论基础:单元测试、集成测试、TDD/BDD 方法论
- https://plumephp.com/posts/devops/ — CI/CD 流水线设计:GitHub Actions 与 Jenkins 测试门禁
- https://plumephp.com/posts/cs-fundamentals/ — 测试驱动开发形式化验证的理论基础
文末完整示例
// 完整可运行示例:Google Test + 自定义 Matcher + 参数化测试
// 编译:g++ -std=c++20 -lgtest -lgtest_main -pthread -o test_demo test_demo.cpp
// 运行:./test_demo
#include <gtest/gtest.h>
#include <vector>
#include <algorithm>
#include <numeric>
// ====== 被测函数 ======
std::vector<int> filter_even(const std::vector<int>& input) {
std::vector<int> result;
for (int x : input) {
if (x % 2 == 0) result.push_back(x);
}
return result;
}
double compute_average(const std::vector<int>& values) {
if (values.empty()) return 0.0;
return static_cast<double>(std::accumulate(values.begin(), values.end(), 0))
/ values.size();
}
class StringUtils {
public:
static bool is_palindrome(const std::string& s) {
return std::equal(s.begin(), s.begin() + s.size() / 2, s.rbegin());
}
};
// ====== Google Test 测试 ======
TEST(FilterTest, BasicEvenFiltering) {
std::vector<int> input{1, 2, 3, 4, 5, 6};
auto result = filter_even(input);
EXPECT_EQ(result, (std::vector<int>{2, 4, 6}));
}
TEST(FilterTest, EmptyInput) {
auto result = filter_even({});
EXPECT_TRUE(result.empty());
}
TEST(MathTest, AverageCalculation) {
EXPECT_DOUBLE_EQ(compute_average({1, 2, 3, 4}), 2.5);
EXPECT_DOUBLE_EQ(compute_average({}), 0.0);
EXPECT_DOUBLE_EQ(compute_average({42}), 42.0);
}
TEST(StringUtilsTest, Palindromes) {
EXPECT_TRUE(StringUtils::is_palindrome("racecar"));
EXPECT_TRUE(StringUtils::is_palindrome(""));
EXPECT_TRUE(StringUtils::is_palindrome("a"));
EXPECT_FALSE(StringUtils::is_palindrome("hello"));
EXPECT_TRUE(StringUtils::is_palindrome("A man, a plan, a canal: Panama")); // 注意:未处理空格和大小写
}
// ====== 参数化测试示例 ======
class SquareParamTest : public ::testing::TestWithParam<std::pair<int, int>> {};
TEST_P(SquareParamTest, ComputesCorrectly) {
auto [input, expected] = GetParam();
EXPECT_EQ(input * input, expected);
}
INSTANTIATE_TEST_SUITE_P(
SquareValues,
SquareParamTest,
::testing::Values(
std::make_pair(0, 0),
std::make_pair(1, 1),
std::make_pair(-5, 25),
std::make_pair(12, 144)
)
);
// ====== 测试夹具示例 ======
class AccumulatorTest : public ::testing::Test {
protected:
std::vector<int> values_;
void SetUp() override {
values_ = {10, 20, 30, 40, 50};
}
};
TEST_F(AccumulatorTest, SumIsCorrect) {
int sum = std::accumulate(values_.begin(), values_.end(), 0);
EXPECT_EQ(sum, 150);
}
TEST_F(AccumulatorTest, AverageIsCorrect) {
EXPECT_DOUBLE_EQ(compute_average(values_), 30.0);
}
// 注意:此示例需要链接 gtest_main 以提供 main 函数
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。