C++ GoogleTest
简介
GoogleTest(以下简称 GTest) 是 Google 开发的一个开源 C++ 测试框架,它具有跨平台性,能够在不同的操作系统和编译器上运行。GTest 为开发人员提供了丰富的断言、测试夹具和测试发现机制,帮助你创建更加健壮的代码。
在软件开发中,测试是确保代码正确性和可靠性的重要环节。通过单元测试,我们可以验证单个函数或类的行为是否符合预期,及早发现并修复 bug,这对于代码的维护和重构也非常有帮助。
单元测试是测试软件中的最小可测试单元(如函数、方法或类)的过程,目的是验证它们是否按照预期工作。
为什么选择 GoogleTest?
- 丰富的断言:提供了大量的断言宏,可以测试各种条件
- 死亡测试:可以验证代码是否按预期崩溃
- 参数化测试:可以使用不同的参数运行相同的测试
- 测试夹具:方便设置和清理测试环境
- 良好的错误报告:当测试失败时提供详细的错误信息
- 跨平台支持:可在 Windows、Linux、Mac OS X 等平台上使用
安装 GoogleTest
使用 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 源代码并手动编译:
- 从 GitHub 克隆存储库:
git clone https://github.com/google/googletest.git
- 按照 README 中的说明进行编译和安装
第一个 GoogleTest 程序
让我们从一个简单的例子开始,测试一个计算两个数之和的函数:
首先,创建一个 math.h
文件:
#ifndef MATH_H
#define MATH_H
// 返回两个整数的和
int Add(int a, int b) {
return a + b;
}
#endif // MATH_H
然后,创建测试文件 math_test.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()
宏定义,接受两个参数:测试套件名和测试名 - 测试套件:相关测试的集合,具有相同的第一个参数
TEST(TestSuiteName, TestName) {
// 测试代码
}
2. 断言
GoogleTest 提供两类断言:
断言类型 | 成功继续 | 失败终止当前函数 |
---|---|---|
致命断言 | ASSERT_* | 是 |
非致命断言 | EXPECT_* | 否 |
常用的断言:
// 基本断言
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)
当多个测试需要相似的数据或设置时,可以使用测试夹具:
#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);
}
参数化测试
当需要用多组参数测试相同的代码时,参数化测试非常有用:
#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)
)
);
死亡测试
死亡测试用于验证代码是否按照预期方式崩溃:
#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
:
// 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
现在,为这个类编写全面的测试:
// 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();
}
最佳实践
- 测试代码分离:将测试代码与生产代码分开,通常放在单独的目录或文件中
- 命名规范:使用清晰一致的命名约定,如
Test<类名>_<功能>
- 测试一个行为:每个测试只测试一个具体行为,让测试更容易理解和维护
- 使用测试夹具:当需要重用测试代码和数据时,使用测试夹具
- 表驱动测试:对于需要多组输入的测试,使用参数化测试
- 不要过度测试:测试应该验证行为而非实现细节
- 清理资源:总是在测试后清理创建的资源,避免内存泄漏
实用技巧
筛选测试
可以通过命令行参数运行特定的测试:
./my_test --gtest_filter=TestSuiteName.TestName
使用通配符:
./my_test --gtest_filter=MyString*
生成 XML 报告
./my_test --gtest_output=xml:test_results.xml
重复测试
对于难以重现的问题,可以重复运行测试:
./my_test --gtest_repeat=100
总结
GoogleTest 是一个强大的 C++ 测试框架,能够帮助开发者编写清晰、维护性好的单元测试。通过掌握本文介绍的基本概念和技术,你可以:
- 编写基本的测试用例
- 使用各种断言验证代码行为
- 创建测试夹具来重用测试代码
- 利用参数化测试测试多组输入
- 使用死亡测试验证错误处理
记住,良好的测试不仅能够帮助你捕获代码中的错误,还能让你对代码重构更有信心,促进更好的代码设计。
练习
- 使用 GoogleTest 为一个简单的计算器类编写测试,包括加法、减法、乘法和除法操作。
- 创建一个测试夹具,用于测试一个简单的栈或队列实现。
- 为字符串反转函数编写参数化测试,测试不同的输入字符串。
- 编写死亡测试,验证除以零时抛出的异常。
附加资源
通过这些资源和练习,你可以进一步增强你的 C++ 测试能力,编写更加健壮和可维护的代码。