跳到主要内容

C++ GoogleTest

简介

GoogleTest(以下简称 GTest) 是 Google 开发的一个开源 C++ 测试框架,它具有跨平台性,能够在不同的操作系统和编译器上运行。GTest 为开发人员提供了丰富的断言、测试夹具和测试发现机制,帮助你创建更加健壮的代码。

在软件开发中,测试是确保代码正确性和可靠性的重要环节。通过单元测试,我们可以验证单个函数或类的行为是否符合预期,及早发现并修复 bug,这对于代码的维护和重构也非常有帮助。

提示

单元测试是测试软件中的最小可测试单元(如函数、方法或类)的过程,目的是验证它们是否按照预期工作。

为什么选择 GoogleTest?

  • 丰富的断言:提供了大量的断言宏,可以测试各种条件
  • 死亡测试:可以验证代码是否按预期崩溃
  • 参数化测试:可以使用不同的参数运行相同的测试
  • 测试夹具:方便设置和清理测试环境
  • 良好的错误报告:当测试失败时提供详细的错误信息
  • 跨平台支持:可在 Windows、Linux、Mac OS X 等平台上使用

安装 GoogleTest

使用 CMake

使用 CMake 是最简单的方式:

cmake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(my_project)

# GoogleTest 需要至少 C++11
set(CMAKE_CXX_STANDARD 11)

include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.11.0
)
FetchContent_MakeAvailable(googletest)

# 添加测试可执行文件
add_executable(
hello_test
hello_test.cpp
)
target_link_libraries(
hello_test
gtest_main
)

# 添加测试
include(GoogleTest)
gtest_discover_tests(hello_test)

手动安装

也可以下载 GoogleTest 源代码并手动编译:

  1. 从 GitHub 克隆存储库: git clone https://github.com/google/googletest.git
  2. 按照 README 中的说明进行编译和安装

第一个 GoogleTest 程序

让我们从一个简单的例子开始,测试一个计算两个数之和的函数:

首先,创建一个 math.h 文件:

cpp
#ifndef MATH_H
#define MATH_H

// 返回两个整数的和
int Add(int a, int b) {
return a + b;
}

#endif // MATH_H

然后,创建测试文件 math_test.cpp

cpp
#include <gtest/gtest.h>
#include "math.h"

// 测试 Add 函数
TEST(MathTest, AdditionWorks) {
// 测试基本的加法
EXPECT_EQ(Add(1, 1), 2);
EXPECT_EQ(Add(10, 20), 30);

// 测试负数加法
EXPECT_EQ(Add(-1, 1), 0);
EXPECT_EQ(Add(-10, -20), -30);
}

// 主函数
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

编译并运行后,你将看到如下输出:

[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from MathTest
[ RUN ] MathTest.AdditionWorks
[ OK ] MathTest.AdditionWorks (0 ms)
[----------] 1 test from MathTest (0 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.

GoogleTest 的基本概念

1. 测试和测试套件

  • 测试:使用 TEST() 宏定义,接受两个参数:测试套件名和测试名
  • 测试套件:相关测试的集合,具有相同的第一个参数
cpp
TEST(TestSuiteName, TestName) {
// 测试代码
}

2. 断言

GoogleTest 提供两类断言:

断言类型成功继续失败终止当前函数
致命断言ASSERT_*
非致命断言EXPECT_*

常用的断言:

cpp
// 基本断言
EXPECT_TRUE(condition); // 期望条件为真
EXPECT_FALSE(condition); // 期望条件为假

// 二元比较
EXPECT_EQ(expected, actual); // 期望相等 (==)
EXPECT_NE(val1, val2); // 期望不相等 (!=)
EXPECT_LT(val1, val2); // 期望小于 (<)
EXPECT_LE(val1, val2); // 期望小于等于 (<=)
EXPECT_GT(val1, val2); // 期望大于 (>)
EXPECT_GE(val1, val2); // 期望大于等于 (>=)

// 字符串比较
EXPECT_STREQ(str1, str2); // 期望两个C字符串内容相同
EXPECT_STRNE(str1, str2); // 期望两个C字符串内容不同
EXPECT_STRCASEEQ(str1, str2); // 期望两个C字符串内容相同(忽略大小写)
EXPECT_STRCASENE(str1, str2); // 期望两个C字符串内容不同(忽略大小写)
警告

使用 ASSERT_* 时要注意,如果断言失败,当前函数会立即返回,不会执行后续代码。

测试夹具 (Test Fixtures)

当多个测试需要相似的数据或设置时,可以使用测试夹具:

cpp
#include <gtest/gtest.h>
#include <vector>

// 定义测试夹具类
class VectorTest : public ::testing::Test {
protected:
// 在每个测试之前运行
void SetUp() override {
v1.push_back(1);
v1.push_back(2);
v2.push_back(3);
v2.push_back(4);
}

// 在每个测试之后运行
void TearDown() override {
// 清理资源
}

// 测试所需的成员
std::vector<int> v1;
std::vector<int> v2;
};

// 使用 TEST_F 而不是 TEST
TEST_F(VectorTest, SizeCheck) {
EXPECT_EQ(v1.size(), 2U);
EXPECT_EQ(v2.size(), 2U);
}

TEST_F(VectorTest, ContentsCheck) {
EXPECT_EQ(v1[0], 1);
EXPECT_EQ(v2[1], 4);
}

参数化测试

当需要用多组参数测试相同的代码时,参数化测试非常有用:

cpp
#include <gtest/gtest.h>
#include "math.h"

// 参数化测试类
class AdditionTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {
};

// 测试 Add 函数的参数化测试
TEST_P(AdditionTest, CheckAddition) {
int a = std::get<0>(GetParam());
int b = std::get<1>(GetParam());
int expected = std::get<2>(GetParam());

EXPECT_EQ(Add(a, b), expected);
}

// 实例化参数化测试
INSTANTIATE_TEST_SUITE_P(
AdditionTests,
AdditionTest,
::testing::Values(
std::make_tuple(1, 1, 2),
std::make_tuple(10, 20, 30),
std::make_tuple(-1, 1, 0),
std::make_tuple(-10, -20, -30)
)
);

死亡测试

死亡测试用于验证代码是否按照预期方式崩溃:

cpp
#include <gtest/gtest.h>

void DieFunction() {
throw std::runtime_error("Intentional crash");
}

// 死亡测试
TEST(DeathTest, TestDeath) {
// 验证函数是否抛出异常
EXPECT_THROW(DieFunction(), std::runtime_error);

// 另一种写法,验证函数是否触发终止
EXPECT_DEATH(abort(), ".*");

// 验证特定消息的异常
EXPECT_EXIT(exit(1), ::testing::ExitedWithCode(1), "");
}

实际案例:测试自定义字符串类

让我们通过一个真实的例子来了解如何使用 GoogleTest 测试自定义字符串类:

首先,定义一个简单的字符串类 MyString

cpp
// mystring.h
#ifndef MYSTRING_H
#define MYSTRING_H

#include <cstring>
#include <algorithm>

class MyString {
public:
MyString() : data_(nullptr), size_(0) {}

explicit MyString(const char* str) {
if (str == nullptr) {
data_ = nullptr;
size_ = 0;
} else {
size_ = strlen(str);
data_ = new char[size_ + 1];
memcpy(data_, str, size_ + 1);
}
}

MyString(const MyString& other) {
size_ = other.size_;
if (size_ == 0) {
data_ = nullptr;
} else {
data_ = new char[size_ + 1];
memcpy(data_, other.data_, size_ + 1);
}
}

~MyString() {
delete[] data_;
}

MyString& operator=(const MyString& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
if (size_ == 0) {
data_ = nullptr;
} else {
data_ = new char[size_ + 1];
memcpy(data_, other.data_, size_ + 1);
}
}
return *this;
}

size_t size() const { return size_; }

const char* c_str() const { return data_; }

bool operator==(const MyString& other) const {
if (size_ != other.size_) return false;
if (data_ == nullptr && other.data_ == nullptr) return true;
if (data_ == nullptr || other.data_ == nullptr) return false;
return strcmp(data_, other.data_) == 0;
}

private:
char* data_;
size_t size_;
};

#endif // MYSTRING_H

现在,为这个类编写全面的测试:

cpp
// mystring_test.cpp
#include <gtest/gtest.h>
#include "mystring.h"

// 基本测试:构造和析构
TEST(MyStringTest, ConstructorAndDestructor) {
// 默认构造函数
MyString s1;
EXPECT_EQ(s1.size(), 0U);
EXPECT_EQ(s1.c_str(), nullptr);

// 从 C 字符串构造
MyString s2("Hello");
EXPECT_EQ(s2.size(), 5U);
EXPECT_STREQ(s2.c_str(), "Hello");

// 空字符串
MyString s3("");
EXPECT_EQ(s3.size(), 0U);
EXPECT_STREQ(s3.c_str(), "");

// 从 nullptr 构造
MyString s4(nullptr);
EXPECT_EQ(s4.size(), 0U);
EXPECT_EQ(s4.c_str(), nullptr);
}

// 测试复制构造函数
TEST(MyStringTest, CopyConstructor) {
MyString s1("Hello");
MyString s2(s1);

// 验证内容相同但不是同一个地址
EXPECT_STREQ(s2.c_str(), "Hello");
EXPECT_NE(s1.c_str(), s2.c_str());
}

// 测试赋值运算符
TEST(MyStringTest, AssignmentOperator) {
MyString s1("Hello");
MyString s2;
s2 = s1;

// 验证内容相同但不是同一个地址
EXPECT_STREQ(s2.c_str(), "Hello");
EXPECT_NE(s1.c_str(), s2.c_str());

// 自赋值测试
s1 = s1;
EXPECT_STREQ(s1.c_str(), "Hello");
}

// 测试相等运算符
TEST(MyStringTest, EqualityOperator) {
MyString s1("Hello");
MyString s2("Hello");
MyString s3("World");
MyString s4;
MyString s5;

EXPECT_TRUE(s1 == s2); // 相同内容
EXPECT_FALSE(s1 == s3); // 不同内容
EXPECT_TRUE(s4 == s5); // 两个空字符串
EXPECT_FALSE(s1 == s4); // 一个空一个非空
}

// 使用测试夹具进行更复杂的测试
class MyStringTest : public ::testing::Test {
protected:
void SetUp() override {
empty_ = new MyString();
hello_ = new MyString("Hello");
world_ = new MyString("World");
}

void TearDown() override {
delete empty_;
delete hello_;
delete world_;
}

MyString* empty_;
MyString* hello_;
MyString* world_;
};

TEST_F(MyStringTest, SizeIsCorrect) {
EXPECT_EQ(empty_->size(), 0U);
EXPECT_EQ(hello_->size(), 5U);
EXPECT_EQ(world_->size(), 5U);
}

TEST_F(MyStringTest, CStrIsCorrect) {
EXPECT_EQ(empty_->c_str(), nullptr);
EXPECT_STREQ(hello_->c_str(), "Hello");
EXPECT_STREQ(world_->c_str(), "World");
}

int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

最佳实践

  1. 测试代码分离:将测试代码与生产代码分开,通常放在单独的目录或文件中
  2. 命名规范:使用清晰一致的命名约定,如 Test<类名>_<功能>
  3. 测试一个行为:每个测试只测试一个具体行为,让测试更容易理解和维护
  4. 使用测试夹具:当需要重用测试代码和数据时,使用测试夹具
  5. 表驱动测试:对于需要多组输入的测试,使用参数化测试
  6. 不要过度测试:测试应该验证行为而非实现细节
  7. 清理资源:总是在测试后清理创建的资源,避免内存泄漏

实用技巧

筛选测试

可以通过命令行参数运行特定的测试:

bash
./my_test --gtest_filter=TestSuiteName.TestName

使用通配符:

bash
./my_test --gtest_filter=MyString*

生成 XML 报告

bash
./my_test --gtest_output=xml:test_results.xml

重复测试

对于难以重现的问题,可以重复运行测试:

bash
./my_test --gtest_repeat=100

总结

GoogleTest 是一个强大的 C++ 测试框架,能够帮助开发者编写清晰、维护性好的单元测试。通过掌握本文介绍的基本概念和技术,你可以:

  1. 编写基本的测试用例
  2. 使用各种断言验证代码行为
  3. 创建测试夹具来重用测试代码
  4. 利用参数化测试测试多组输入
  5. 使用死亡测试验证错误处理

记住,良好的测试不仅能够帮助你捕获代码中的错误,还能让你对代码重构更有信心,促进更好的代码设计。

练习

  1. 使用 GoogleTest 为一个简单的计算器类编写测试,包括加法、减法、乘法和除法操作。
  2. 创建一个测试夹具,用于测试一个简单的栈或队列实现。
  3. 为字符串反转函数编写参数化测试,测试不同的输入字符串。
  4. 编写死亡测试,验证除以零时抛出的异常。

附加资源

通过这些资源和练习,你可以进一步增强你的 C++ 测试能力,编写更加健壮和可维护的代码。