GoogleTest简单使用
来自Google的C++单测工具
背景介绍
单元测试
基本概念
单元测试(Unit Testing)是软件开发过程中的一种测试方法,用于验证代码中的最小可测试单元(通常是函数、方法或类)是否按预期工作。单元测试的目的是确保每个单元在独立运行时都能正确执行其功能,从而提高代码的质量和可靠性。
代码覆盖率
- 定义:代码覆盖率是指单元测试执行时覆盖的代码行数、分支数或条件数占总代码的比例。
- 常见指标:
- 行覆盖率:测试覆盖了多少行代码。
- 分支覆盖率:测试覆盖了多少个分支(如
if-else
语句)。 - 条件覆盖率:测试覆盖了多少个条件表达式。
FIRST原则
- Fast (快速):单元测试应该快速执行,避免影响开发效率;
- Independent (独立):单元测试应该相互独立,避免相互影响;
- Repeatable (可重复):单元测试应该可以在任何环境下重复执行,并得到相同的结果;
- Self-Validating (自验证):单元测试应该能够自动验证测试结果,无需人工干预;
- Thorough (彻底):单元测试应该覆盖所有重要的代码路径。
GoogleTest
简介
GoogleTest(通常称为GTest)是一个由Google开发的C++测试框架,用于编写和运行单元测试。它是一个开源项目,广泛用于测试C++代码,支持跨平台开发,包括Linux、macOS和Windows。
特点
- 简单易用:GTest提供了简洁的API,使得编写测试用例变得简单直观。
- 丰富的断言库:提供了多种断言宏(如
EXPECT_EQ
、ASSERT_TRUE
等),用于验证代码的预期行为。 - 值参数化测试:允许使用不同的输入参数运行相同的测试用例。
- 类型参数化测试:允许针对不同类型运行相同的测试用例。
- 测试夹具:支持使用测试夹具来设置和清理测试环境。
- 事件监听器:允许用户自定义测试运行时的行为,如记录日志、生成报告等。
- 跨平台支持:支持多种操作系统和编译器。
GTest使用
基本概念
- 断言(assertion):判断给定条件是否为真,如果为真则继续执行,不为真则会报错并中止程序,测试时通常大量使用断言来判断执行过程是否符合预期;
- 测试用例(test case):基本测试单元,用来测试单个函数或者功能模块的正确性;
- 测试套件(test suite):包含一个或多个测试用例,通常用来测试同一个类的不同方法或者同一个模块的不同子模块;
- 测试夹具(test fixture):特殊的测试套件,需要另外定义测试类,所有的测试用例共享该测试类方法,主要用来降低代码冗余。
简单测试
使用TEST()
宏定义和命名测试函数。
TEST()
是不带返回值的普通C++函数;- 测试的结果由
TEST()
函数体设置的断言决定,任何一个断言失败都会导致整个测试不通过; - 每个测试用例内部可以通过使用
{}
限制作用域来设置多个子case。
// TestSuiteName_TestName组合需要全局唯一
TEST(TestSuiteName, TestName) {
... test body ...
}
举个例子:
TEST(Foo, bar) {
// case 1: enable = true
{
Context ctx;
params.enable_refresh = true;
EXPECT_EQ(ctx->is_enable_fresh(), true);
}
// case 2: enable = false
{
Context ctx;
params.enable_refresh = false;
EXPECT_EQ(ctx->is_enable_fresh(), false);
}
}
测试夹具
使用TEST_F()
宏定义和命名测试函数。在此之前需要创建对应的测试类,该测试类要继承自testing::Test
。
class TestFixtureClassName : public testing::Test { // public勿忘
public:
void SetUp() override {
// 初始化代码
// 每个测试用例创建时要做的工作
}
void TearDown() override {
// 析构时代码
// 每个测试对象析构时要做的工作
}
void func() {} // 自定义方法
int val; // 自定义成员变量
};
// 每个测试用例都会初始化一个测试类对象
// 不同测试用例的对象互不影响
TEST_F(TestFixtureClassName, TestName) {
... test body ...
}
举个例子:
class FooTest : public ::testing::Test {
protected:
// 在每个 Test Case 运行开始前,都会调用 SetUp,这里可以初始化
void SetUp() override {
ctx = RequestContext("123");
}
// 在每个 Test Case 运行结束后,都会调用 TearDown
void TearDown() override {}
// 所有 Test Case 都可以直接访问这些变量和方法
Ad new_ad() { return Ad(ctx); }
RequestContext ctx;
};
TEST_F(FooTest, enable_foo) { // 这里会初始化 FooTest 对象
ctx->params.enable_foo = true; // 可以访问 FooTest 中的变量
auto item = new_ad(); // 可以调用 FooTest 中的方法
...
}
// 每个 test case 都是独立的,这里会初始化另一个 FooTest 对象
TEST_F(FooTest, OnTestProgramStart) {
// ...
}
常用断言
- 布尔值检查:
EXPECT_TRUE(condition)
:EXPECT_FALSE(condition)
;
- 整数值检查(后缀N-not,E(Q)-equal,L-less,T-than,G-greater):
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_FLOAT_EQ(expected, actual)
;EXPECT_DOUBLE_EQ(expected, actual)
。
个人用的最多的就是以上这些,除此之外还有像字符串检查,异常检查,类型检查等等可以参考官方文档
启动测试
GTest提供了main
方法,只需要链接GTest::gtest_main
库即可,这是官方推荐的做法。也可以自己在测试文件中加上main
函数,其中调用RUN_ALL_TESTS()
宏作为返回值。自定义main
函数里面有个坑,后文会提到。
int main(int argc, char **argv) {
// 传递googletest配置相关的flag
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
RUN_ALL_TESTS()
如果返回0
,表示测试通过,否则是测试未通过。其具体做的工作包括:
- 读取并保存由
main
函数传入的临时flag信息; - 为第一个测试用例创建测试夹具对象(简单测试也会创建测试套件对象);
- 调用
SetUp()
初始化该对象; - 运行该测试用例;
- 调用
TearDown()
清理对象内存; - 删除对象;
- 恢复原来的flag信息;
- 重复上述过程直到所有的测试用例执行完成。
注意RUN_ALL_TESTS()
只能调用一次。
GMock使用
GMock是GTest提供的C++ Mock框架,支持创建虚拟的类的对象。在开发过程中,可能存在要测试的对象所依赖的测试环境不易复现,或者所依赖的类尚未开发完成的情况,此时用户通过Mock的方式自定义该依赖类中方法的输入和对应的执行结果来进行模拟测试环境,以顺利完成当前对象的测试工作。
接下来介绍GMock的使用流程。
定义原型类接口
假设有个作图的类Turtle
还未开发完成,该类定义的接口如下:
class Turtle {
...
// 析构函数必须要是虚函数
virtual ~Turtle() {}
// 无参数方法
virtual void PenUp() = 0;
virtual void PenDown() = 0;
// 但参数方法
virtual void Forward(int distance) = 0;
virtual void Turn(int degrees) = 0;
// 多参数方法
virtual void GoTo(int x, int y) = 0;
// 带const属性方法
virtual int GetX() const = 0;
virtual int GetY() const = 0;
};
定义Mock类
#include <gmock/gmock.h> // Brings in gMock.
class MockTurtle : public Turtle {
public:
...
MOCK_METHOD(void, PenUp, (), (override));
MOCK_METHOD(void, PenDown, (), (override));
MOCK_METHOD(void, Forward, (int distance), (override));
MOCK_METHOD(void, Turn, (int degrees), (override));
MOCK_METHOD(void, GoTo, (int x, int y), (override));
MOCK_METHOD(int, GetX, (), (const, override));
MOCK_METHOD(int, GetY, (), (const, override));
};
Mock类定义步骤:
- Mock类
MockTurtle
需要继承自原型类; - 所有方法属性为
public
; - 对于纯虚类
Trutle
中的所有方法在MockTrutle
中添加对应的MOCK_METHOD
方法,其参数分别为:- 返回值;
- 方法名;
- (形参),如果形参中存在泛型需要再加一层括号,例如
MOCK_METHOD(void, funName, (int, (map<int,string>)),(override))
; - (方法属性),包括
const/override
等,这里并不是强制的。
使用Mock对象
#include "path/to/mock-turtle.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using ::testing::AtLeast; // #1
TEST(PainterTest, CanDrawSomething) {
MockTurtle turtle; // #2
EXPECT_CALL(turtle, PenDown()) // #3
.Times(AtLeast(1));
Painter painter(&turtle); // #4
EXPECT_TRUE(painter.DrawCircle(0, 0, 10)); // #5
}
- 从
::testing
命名空间中引入后文需要使用的方法(#1
),这里需要使用AtLeast
所以需要引入是代码更简洁; - 创建mock对象(
#2
),这里创建了对象turtle
; - 设置调用对象方法的期望输入及返回值(expection),即自定义方法的输入输出(
#3
),这里设置了turtle.PenDown()
方法会被调用至少一次; - 根据mock对象创建测试类对象(
#4
),这里使用turtle
创建了测试类Painter
对象painter
; - 设置对应的断言判断测试类方法执行是否符合期望(
#5
),这里painter.DrawCircle()
方法会调用turtle.PenDown()
方法至少一次。
注意GMock
要求方法的期望定义必须在方法被调用之前,否则会出现undefined behavior错误。
设置期望
GMock使用宏EXPECT_CALL()
类定义一种期望(expection),其语法格式为:
EXPECT_CALL(mock_object, method(matchers))
.Times(cardinality)
.WillOnce(action)
.WillRepeatedly(action);
宏定义中有传入两个参数:mock_object
表示mock对象,method(matchers)
表示调用的方法+参数匹配器(matcher
),如果没有形参可以省略括号简写为method
:
EXPECT_CALL(mock_object, non-overloaded-method)
.Times(cardinality)
.WillOnce(action)
.WillRepeatedly(action);
宏定义中调用的方法Times()
、WillOnce()
、WillRepeatedly()
其作用基本与其方法名含义一致,这里举个例子:
using ::testing::Return;
...
EXPECT_CALL(turtle, GetX())
.Times(5)
.WillOnce(Return(100))
.WillOnce(Return(150))
.WillRepeatedly(Return(200));
表示会调用turtle.GetX()
方法5次,返回值分别为:100、150、200、200和200。具体含义会在后文介绍。
匹配传入参数
GMock使用matcher匹配传入参数,matcher可以是一个确定的值:
// 表示Forward方法传入的参数为100
EXPECT_CALL(turtle, Forward(100));
matcher也可以包含通配符_
,表示任意值:
// 表示GoTo方法传入两个参数,其中第一个是50,第二个是任意的
EXPECT_CALL(turtle, GoTo(50, _));
matcher还可以是内置的范围匹配(Eq/Ge/Gt/Le/Lt/Ne
等)或者自定义范围匹配:
// 表示Forward方法传入的参数为大于100的数
EXPECT_CALL(turtle, Forward(Ge(100)));
matcher也可以为空,前提是方法没有被重载:
// 不需要任何输入参数
EXPECT_CALL(turtle, Forward);
EXPECT_CALL(turtle, GoTo);
定义调用次数
宏EXPECT_CALL
调用的Times()
方法用来定义该方法被调用的次数,可以接收的参数包括确定的数字或者内置的范围匹配:Times(n)
表示调用n次,Times(AtLeast(n))
表示至少调用n次。
Times()
也可以被忽略,GMock会根据后面的Will
方法调用来推导:
- 如果
EXPECT_CALL
后既没有调用WillOnce
,也没有调用WillRepeatedly
,则Times()
被推导为Times(1)
; - 如果
EXPECT_CALL
后调用了n次WillOnce
而没有调用WillRepeatedly
,则Times()
被推导为Times(n)
; - 如果
EXPECT_CALL
后调用了n次WillOnce
,又调用了WillRepeatedly
,则Times()
被推导为Times(AtLeast(n))
。
定义返回值
Return
宏用来定义方法的返回值,如果未指定则返回类型的默认值(bool
→false
,int
→0
等),举个例子:
// GetX方法调用三次,返回值分别为100/200/300
EXPECT_CALL(turtle, GetX())
.WillOnce(Return(100))
.WillOnce(Return(200))
.WillOnce(Return(300));
// GetY方法调用不少于两次,返回值分别为100/200/300/300...
EXPECT_CALL(turtle, GetY())
.WillOnce(Return(100))
.WillOnce(Return(200))
.WillRepeatedly(Return(300));
注意WillRepeatedly传入的参数只会被接收一次,也就是说当方法被多次调用时返回值都是一样的,举个例子:
int n = 100;
EXPECT_CALL(turtle, GetX())
.Times(4)
.WillRepeatedly(Return(n++)); // 每次返回值都是100,而不是100/101/102...
处理多个预期
如果一个方法设置了多个期望,该方法被调用时GMock会按照逆序找到匹配该调用的mock方法,举个例子:
EXPECT_CALL(turtle, Forward(_)); // #1
EXPECT_CALL(turtle, Forward(10)) // #2
.Times(2);
如果Forward
方法调用顺序为Forward(10)→Forward(10)→Forward(10)
,则第三次调用时会发生错误,因为每次调用Forward(10)
都匹配的期望#2
(粘连特性),导致第三次调用时期望#2
已经用完,所以会提示错误。如果调用顺序为Forward(10)→Forward(10)→Forward(20)
则可以正常通过。
关于期望的粘连特性,这里还有个例子:
for (int i = n; i > 0; i--) {
EXPECT_CALL(turtle, GetX())
.WillOnce(Return(10*i));
}
如果调用了n
次GetX()
函数得到的返回值什么?是10/20/…/10n
?
都不是,上述代码在第二次调用的时候会报错,因为每次调用传入的参数都是相同的(为空),所有调用都匹配的是最后一个期望。
如果想实现不同的返回值,则需要进行修改:
for (int i = n; i > 0; i--) {
EXPECT_CALL(turtle, GetX())
.WillOnce(Return(10*i))
.RetiresOnSaturation();
}
也可以添加顺序限制来实现:
{
InSequence s;
// 期望可以直接按照顺序设置
for (int i = 1; i <= n; i++) {
EXPECT_CALL(turtle, GetX())
.WillOnce(Return(10*i))
.RetiresOnSaturation();
}
}
限制调用顺序
默认情况下不同方法的期望定义顺序与调用顺序不需要一致,如果需要严格限制二者顺序一致,则需要使用添加InSequence
对象进行限制,该对象的作用域中期望定义顺序需要与调用顺序一致,举个例子:
TEST(FooTest, DrawsLineSegment) {
...
{
InSequence seq;
// 调用顺序必须为:PenDown()->Forward(100)->PenUp()
EXPECT_CALL(turtle, PenDown());
EXPECT_CALL(turtle, Forward(100));
EXPECT_CALL(turtle, PenUp());
}
Foo();
}
搭配CMake使用
CMake其实有原生支持的测试模块CTest,但是总体上比较鸡肋,测试用例设置较为繁琐,功能上也远不如GTest丰富。CMake后来引入了GTest模块的支持,通过识别测试文件中的gtest相关符号来自动设置生成CTest,这里主要介绍下CMake和GTest的结合使用。
gtest_add_tests
函数
CMake3.9引入的GTest模块提供了gtest_add_tests()
函数,其在cmake配置阶段通过扫描测试源文件中GTest的宏函数来获取所有的GTest测试用例,然后生成对应的CTest配置文件CTestTestfile.cmake
。
gtest_add_tests()
函数的定义如下:
gtest_add_tests(
TARGET target # 设置测试目标
[SOURCES src1...] # 指定扫描的源文件
[EXTRA_ARGS args...]
[WORKING_DIRECTORY dir]
[TEST_PREFIX prefix]
[TEST_SUFFIX suffix]
[SKIP_DEPENDENCY]
[TEST_LIST outVar]
)
举个例子:
# 设置测试目标
add_executable(testapp test.cpp)
# 链接GTest库
target_link_libraries(testapp PRIVATE
GTest::gtest_main
GTest::gmock_main
)
include(GoogleTest)
# 绑定GTest测试目标以及对应的源文件
gtest_add_tests(
TARGET testapp
SOURCE test.cpp
)
# 开启测试选项
enable_testing()
在构建完成后,运行CTest
命令即可执行测试用例。
gtest_discover_tests
函数
gtest_add_tests()
因为是对源码进行扫描的,所以每次修改测试用例后就需要运行CMake重新生成Makefile。为解决这个问题,CMake3.10中新引入了gtest_discover_tests()
函数,该函数会在构建完成后测试执行前(post-build or pre-test)来检测所有的测试用例。具体的做法也很简单,就是利用GTest的--gtest_list_tests
flag执行一次构建得到的测试可执行文件(实际上加上flag后并不会执行而是直接输出所有的测试用例),生成对应的测试配置文件。这样在修改测试用例后就不用再执行CMake了,提高了鲁棒性,很适合参数化测试(parameterized tests)的场景。当然有好处就会有相应的问题,gtest_discover_tests
由于是在构建之后获取的测试样例,就不能像gtest_add_tests
那样在配置阶段设置细粒度的测试相关flag,在交叉编译的场景下会有一定的局限性。
gtest_discover_test()
在获取所有的GTest测试用例后会自动转换成CTest用例。其函数定义:
gtest_discover_tests(
target # 去掉了TARGET关键字,需要与add_executable目标一致
[EXTRA_ARGS args...]
[WORKING_DIRECTORY dir]
[TEST_PREFIX prefix]
[TEST_SUFFIX suffix]
[TEST_FILTER expr]
[NO_PRETTY_TYPES] [NO_PRETTY_VALUES]
[PROPERTIES name1 value1...]
[TEST_LIST var]
[DISCOVERY_TIMEOUT seconds]
[XML_OUTPUT_DIR dir]
[DISCOVERY_MODE <POST_BUILD|PRE_TEST>]
[DISCOVERY_EXTRA_ARGS args...]
)
举个例子:
add_executable(testapp test.cpp)
target_link_libraries(testapp PRIVATE
GTest::gtest_main
GTest::gmock_main
)
include(GoogleTest)
# 不需要添加要扫描的源文件
gtest_discover_tests(testapp)
enable_testing()
注意
使用gtest_discover_tests()
时不支持自定义main
函数,只能链接gtest_main
,否则无法生成CTest文件。另外vscode的CMake Tools插件对该函数的支持也有问题。奇怪的是gtest_add_tests()
倒是一切正常,如果项目不算大的时候,个人还是建议使用gtest_add_tests()
。