一、前言
最近项目中需要对SDK对外接口进行集成测试,这边搭建一套GTest的集成用例框架,确保能够覆盖所有类型的接口,同时对GTest语法进行学习总结;另外i这套GTest集成用例框架内部,会再嵌套插件OpenCppCoverage / gcov,用于对集成用例覆盖率的分析。
【debug问题点】:集成测试
【环境】:Windows、Linux
【使用工具】:GTest、VS、OpenCppCoverage、g++、gcov
二、概述
Google Test (gtest) 是一个由 Google 开发的流行的 C++ 单元测试框架。它为开发者提供了一套强大的工具和功能,用于编写、组织和运行测试用例。gtest 框架支持测试驱动开发(Test-Driven Development,TDD)和行为驱动开发(Behavior-Driven Development,BDD)方法。总结下来,有几个主要功能和特点:
- 丰富的断言
gtest 框架提供了一组丰富的断言宏,用于验证测试结果。这些断言宏包括比较值、检查条件、抛出异常等功能,可以满足各种测试需求; - 灵活的测试组织
gtest 框架允许开发者将测试用例组织成测试套件(Test Suite),并可以嵌套多个测试套件。这样可以更好地组织和管理测试用例,提高测试代码的可读性和可维护性; - 测试夹具支持
gtest 框架提供了测试夹具(Test Fixture 的概念,允许开发者在多个测试用例之间共享资源或设置环境。通过派生测试夹具类并重写 SetUp() 和 TearDown() 方法,开发者可以方便地进行资源的初始化和清理操作; - 完善的编译及平台支持
gtest 框架支持 C++11 及以上的 C++ 标准,支持 Linux、MacOS、Windows 等众多平台,支持 gcc5.0+、clang5.0+、MSVC2015+ 编译器,支持 Bazel 以及 cmake 构建工具; - 丰富的测试报告
gtest 框架生成详细的测试报告,显示每个测试用例的运行结果,包括通过的用例和失败的用例。如果断言失败,报告会提供失败的原因和位置,方便开发者快速定位问题。
三、GTest环境配置
这边默认使用者有GTest源码,如果没有,网上很多地方可以下载。
可参考:https://www.cnblogs.com/djh5520/p/14283361.html
3.1、Windos环境
- 解压后打开msvc文件夹,打开gtest.sln,直接编译即可。
- 默认生成静态链接库
/msvc/gtest/debug :gtestd.lib
/msvc/gtest/release:gtest.lib
3.2、Linux环境
解压后,终端进入到make文件夹,执行make命令,生成gtest_main.a,编译完成。
3.3、用例工程配置
解压Gtest源码包后,进入到/include/gtest, 拷贝其头文件。
- 配置项目属性——头文件
- 配置项目属性——编译方式【与gtest链接库一致】
- 配置项目属性——链接库路径
- 配置项目属性——链接库
四、GTest语法
4.1、简单demo
#include <gtest/gtest.h>
int add(int a, int b) { return a + b; }
TEST(TEST_ADD, UNSIGNED_INT_VALUE) {
int result = add(100, 200);
EXPECT_EQ(result, 300);
result = add(200, 300);
EXPECT_NE(result, 400);
}
int main() {
testing::InitGoogleTest();
return RUN_ALL_TESTS();
}
4.2、测试宏
4.2.1、TEST 宏【TestSuiteName, TestCaseName】
【说明】:该宏定义用来测试其内部代码,其内部断言决定最终的测试结果。
【用途】:针对多个用例之间不需要进行数据共用的测试场景。
TEST(TEST_ADD, UNSIGNED_INT_VALUE) {
bool nRet = add(100); // 测试内容
EXPECT_TRUE(nRet); // 断言
}
TEST(TEST_ADD, NEGATIVE_INT_VALUE) {
bool nRet = add(100);
EXPECT_TRUE(nRet);
}
4.2.2、TEST_P 宏【TestSuiteName, TestCaseName】
【说明】:该宏定义用来参数化测试。
【用途】:当待测试方法的行为取决于传入的参数时,而且这些参数的不同组合有多种, 而你又不想为此写多个类似的 test case 时,可以用参数化测试。
struct TestData {
int a;
int b;
int result;
char type;
};
class CalculateTest : public testing::TestWithParam<TestData> { // 确认参数类型
protected:
void checkData(TestData data) {
// TODO: ...
}
};
`INSTANTIATE_TEST_SUITE_P`(TestMyClassParams, // 确认参数范围
CalculateTest,
::testing::Values(
TestData{100, 200, 300, '+'},
TestData{20, 5, 15, '-'}
));
INSTANTIATE_TEST_SUITE_P(TestMyClassParams,
CalculateTest,
::testing::Range(
TestData{100, 200, 300, '+'},
TestData{20, 5, 15, '-'}
));
TEST_P(CalculateTest, Test) {
TestData data;
bool nRet = checkData(data);
EXPECT_TRUE(nRet);
}
4.2.3、TEST_F 宏【TestSuiteName, TestCaseName】
【说明】:该宏定义用来对 TestFixtureName 类进行多样测试。
【用途】:针对多个用例之间需要进行数据共用的测试场景,用于多样测试,也有助于简化测试代码。
class Student {
public:
Student(int id, std::string name): id_(id), name_(name) {};
~Student() = default;
void SetAge(int age) { age_ = age; }
int GetAge() const { return this->age_; }
void SetScore(int score) { score_ = score; }
int GetScore() const { return this->score_; }
private:
int id_;
std::string name_;
int age_;
int score_;
};
class StudentTest : public testing::Test {
protected:
void SetUp() override {
student = new Student(1234, "Tom");
}
void TearDown() override {
delete student;
}
Student* student;
};
TEST_F(StudentTest, SET_AGE_TEST) {
student->SetAge(16);
int age = student->GetAge();
EXPECT_EQ(age, 16);
}
TEST_F(StudentTest, SET_SCORE_TEST) {
student->SetScore(99);
int score = student->GetScore();
ASSERT_EQ(score, 99);
}
4.3、事件机制
gtest 提供了多种事件机制:
- 全局事件:所有用例执行前后;
- TestSuite 级别:同一 TestSuite 的第一个用例前,最后一个用例执行后;
- TestCase 级别:每个测试用例前后。
4.3.1、全局事件
要实现全局事件,必须写一个类,继承 testing::Environment 类,实现里面的 SetUp 和 TearDown 方法:
- SetUp() 方法在所有用例执行前执行;
- TearDown() 方法在所有用例执行后执行。
class FooEnvironment : public testing::Environment {
public:
virtual void SetUp() {
std::cout << "Foo FooEnvironment SetUP" << std::endl;
}
virtual void TearDown() {
std::cout << "Foo FooEnvironment TearDown" << std::endl;
}
};
int main() {
testing::AddGlobalTestEnvironment(new FooEnvironment); // 事件挂载
testing::InitGoogleTest();
return RUN_ALL_TESTS();
}
4.3.1、TestSuite 事件
要实现 TestSuite 事件,必须写一个类,继承 testing::Test 类,实现 SetUpTestCase 和 TearDownTestCase 方法:
- SetUpTestCase() 方法在 TestSuite 里的第一个 TestCase 之前执行;
- TearDownTestCase() 方法在 TestSuite 里的最后一个 TestCase 之后执行。
class FooTest : public testing::Test {
protected:
static void SetUpTestCase() {
shared_resource_ = new int(1234);
}
static void TearDownTestCase() {
delete shared_resource_;
shared_resource_ = NULL;
}
int* shared_resource_;
};
4.3.1、TestCase 事件
实现方法和 TestSuite 事件几乎一样,只不过它是实现的 SetUp 和 TearDown 方法:
- SetUp() 方法在每一个 TestCase 之前执行;
- TearDown() 方法每一个 TestCase 之后执行。
class FooCalcTest:public testing::Test {
protected:
virtual void SetUp() {
m_foo.Init();
}
virtual void TearDown() {
m_foo.Finalize();
}
FooCalc m_foo;
};
4.4、断言
对测试代码中的检查点进行检查。分为以下两种断言类型:
- ASSERT_*断言:当检查点失败时,退出当前函数;
- EXPECT_*断言:当检查点失败时,继续往下执行。
断言后可以增加自定义信息输出
EXPECT_EQ(a, b) << "Vectors x and y differ at index ";
4.4.1、布尔值检查
4.4.2、字符串类型检查
4.4.3、 数值类型检查
4.4.4、 浮点数检查
4.4.5、返回成功或失败
4.4.6、 Windows 平台的 HRESULT 检查
4.4.7、 类型检查
testing::StaticAssertTypeEq(); 类似模板中的编译检查
4.5、运行参数控制
gtest提供了一系列运行参数,使我们可以对用例的执行进行控制。主要分为两种方式:
- 命令行参数
- 代码中指定FLAG
4.5.1、–gtest_filter
对执行的测试用例进行过滤,支持通配符。 ? 单个字符 * 任意字符 - 排除,如,-a 表示除了 a : 取或,如,a:b 表示 a 或 b。
命令行:--gtest_filter
FLAG:testing::GTEST_FLAG(filter)
--gtest_filter=libtest.*
testing::GTEST_FLAG(filter)= "libtest.*";
4.5.2、–gtest_repeat=[COUNT]
设置用例重复运行次数。
命令行:--gtest_repeat
FLAG: testing::GTEST_FLAG(gtest_repeat)
--gtest_repeat=1000 // 重复执行1000次,即使中途出现错误。
--gtest_repeat=-1 // 无限次数执行
--gtest_repeat=1000 --gtest_filter=FooBar // 重复执行1000次测试用例名称为FooBar的用例。
testing::GTEST_FLAG(gtest_repeat) = 1000;
testing::GTEST_FLAG(filter) = "FooBar";
4.5.3、–gtest_output=xml[:DIRECTORY_PATH|:FILE_PATH]
4.5.3.1、gtest_output使用
输出测试报告,xml格式。
【注意!!!】: 该命令需要在testing::InitGoogleTest(); 前调用,否则不生效,且xml只支持生成一份。
- xml: 不指定输出路径,默认为当前路径
- xml:d:\tmp 输出到d:\tmp目录
- xml:d:\tmp\test.xml 输出到d:\tmp\test.xml目录
命令行:--gtest_output
FLAG: testing::GTEST_FLAG(output)
testing::GTEST_FLAG(output) = "xml:";
4.5.3.2、CustomTestResultProcessorListener监听器
因为GTest只支持生成一份xml,若测试用例过多的话,xml会非常大,不利于查看,这边可以通过GTest的CustomTestResultProcessorListener监听器来实现写多份xml文件。
CustomTestResultProcessorListener 是 Google Test 框架中提供的一个监听器,它允许你为测试结果添加自定义处理逻辑。这个监听器在每个测试用例运行后都会被调用,你可以在这个监听器中添加自定义的处理逻辑,例如将测试结果写入到文件中。
这个监听器是 ::testing::UnitTest::GetInstance()->listeners().Append() 方法添加的,这个方法允许你向测试框架的监听器列表中添加新的监听器。
// 自定义测试结果处理器
bool CustomTestResultProcessor(const ::testing::TestResult& result) {...}
// 设置自定义的测试结果处理器
::testing::UnitTest::GetInstance()->listeners().Append(new ::testing::CustomTestResultProcessorListener(CustomTestResultProcessor));
4.5.4、–gtest_print_time
输出命令行时是否打印每个测试用例的执行时间。默认是打印。
命令行:-gtest_print_time
FLAG: testing::GTEST_FLAG(print_time)
testing::GTEST_FLAG(print_time) = true;
五、GTest结构理解
在Google Test中,Test 和 Environment 是两个不同的概念,它们分别用于不同的目的:
- Test:
测试用例(Test Case):这是一个单元测试,它测试一个特定的功能或行为。
【对应TEST】
测试套件(Test Suite):这是一个测试用例的集合,它可以包含多个测试用例。
【对应testing::Test类】
测试参数(Test Parameter):这是一个测试用例的参数,它允许你为每个测试用例提供不同的输入。
【对应testing::TestWithParam<int>类】 - Environment:
环境(Environment):这是一个测试环境,它可以为测试用例提供一个共享的设置和清理环境。
【对应testing::Environment 类】
以下是一些关于Test和Environment的关键区别:
- 生命周期:
Test:每个测试用例都有自己的生命周期,它在测试开始时创建,在测试结束时销毁。
Environment:每个测试套件都有自己的环境,它在测试开始时创建,在测试结束时销毁。 - 作用域:
Test:测试用例的生命周期仅限于当前测试。
Environment:环境的生命周期是整个测试套件。 - 设置和清理:
Test:测试用例通常不需要设置或清理环境,因为它们是为特定的测试目的而设计的。
Environment:环境通常用于为测试套件提供共享的设置和清理环境。 - 使用方式:
Test:通常在测试套件中使用,用于测试特定的功能或行为。
Environment:通常在测试套件中使用,用于为测试套件提供共享的设置和清理环境。 - 实例化:
Test:通常在测试套件中实例化,每个测试用例都有自己的实例。
Environment:通常在测试套件中实例化,每个测试套件都有自己的环境实例。 - 参数化:
Test:可以使用测试参数来为每个测试用例提供不同的输入。
Environment:通常不直接使用,而是通过环境工厂来创建环境。 - 总的来说,Test 和 Environment 是两个不同的概念,它们分别用于不同的目的。Test 用于测试单个功能或行为,而 Environment 用于为测试套件提供共享的设置和清理环境。
六、OpenCppCoverage代码覆盖工具
这边是写了个bat脚本,执行OpenCppCoverage的命令,如图:
直接执行bat,会在输出res路径中生成index.html,可查看相应代码走到的分支,触发为绿色,未触发为红色。
【注意!!!】:dll和pdb需要都有,代码优化也要关掉,否则生成的html里面是分析不到的。
七、GTest集成测试框架设计
待更新
八、总结
上述学习总结了GTest的一些语法和设计的工程框架,但其实还主要集中在windows下进行实现,后续在linux做二次开发的时候,可以搭建一套Linux环境下的GTest工程,原理大差不大,主要是一些语法使用上的差异。