Google 单元测试框架
Gtest Github
使用 gtest(gmock) 方便我们编写组织 c++ 单元测试。
编译 lib
到 github 拉取代码或者下载某个版本的 zip 包到本地目录,参考 gtest 中的 README.md 如何编译库和编译自己的代码,下面简单介绍下编译方法
手动编译
$ g++ -isystem ${GTEST_DIR}/include -I${GTEST_DIR} \
-pthread -c ${GTEST_DIR}/src/gtest-all.cc
$ ar -rv libgtest.a gtest-all.o
cmake 编译
gtest 已经提供了 cmakelist,可以直接使用cmake 生成 makefile, 编译库和 sample
$ mkdir mybuild # Create a directory to hold the build output.
$ cd mybuild
$ cmake ${GTEST_DIR} # Generate native build scripts.
$ make
然后就可以在编译自己的测试程序时链接 gtest 了。
$ g++ -isystem ${GTEST_DIR}/include -pthread path/to/your_test.cc libgtest.a -o your_test
跟多详细内容参考 readme 和代码中提供的例子(samples ; make 目录下),比如如何解决重复定义宏等问题。
gtest 测试程序
通过 编程参考 和 源码中 sample 目录下的示例
,我们可以很快上手 gtest。gtest 定义了宏供我们写断言语句,一个或者多个断言组成我们的测试用例 case,多个测试用例有时候需要共享一些通用对象,可以把这些用例放在同一个 fixture 中。
断言和 case
gtest 断言提供两个版本
-
ASSERT_*
版本断言,在同一个 case 中(测试函数)中,ASSERT_* 失败就会终止当前用例,开始其他 case ; -
EXPECT_*
版本,当断言失败时,会报错,但是会继续执行剩余语句。
完整的 宏定义, 或见源码 include/gtest/gtest.h
使用哪种语句断言取决自己用例场景,如当前语句失败时后续语句没有继续执行意义,则可以直接使用 ASSERT 终止,否则使用 EXPECT 可以发现更多错误。
如果用例之间不需要什么公用资源,相互独立,可以使用如下方式定义每一个 case
TEST(套件名,用例名)
{
//套件名和用例名自定义
//断言语句
//如一般的c++ 函数,不 return value
}
进入目录 sample 中, 以 sample1_unittest.cc 为例子
#include "sample1.h" // 测试对象头文件,接口
#include "gtest/gtest.h" // gtest 头文件
TEST(IsPrimeTest, Negative) {
EXPECT_FALSE(IsPrime(-1)) << "这样子失败时打印自己的信息";
EXPECT_FALSE(IsPrime(-2)); // 如果此断言失败,还会继续执行下一个
EXPECT_FALSE(IsPrime(INT_MIN));
}
TEST(IsPrimeTest, Negative) {
EXPECT_FALSE(IsPrime(-1));
ASSERT_FALSE(IsPrime(-2)); // 如果此断言失败,下一条不执行,这个case 结束
EXPECT_FALSE(IsPrime(INT_MIN));
}
编译修改的测试代码,其中 libgtest.a
是 gtest 的库。
g++ -isystem ../include/ ./sample1.cc ./sample1_unittest.cc -pthread ../libgtest.a ../libgtest_main.a
链接 libgtest_main.a
是为了使用 src/gtest_main.cc
中定义 main 函数,执行所用测试用例,否者,也可以自己定义 main。
#include <stdio.h>
#include "gtest/gtest.h"
int main(int argc, char **argv) {
printf("Running main() from gtest_main.cc\n");
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
编译后执行输出 bin 直接运行便运行所有用例,可以使用 -h
查看可选的执行参数,如--gtest_filter=IsPrimeTest.Negative
指定执行 套件和 case ; --gtest_output=xml[:DIRECTORY_PATH/|:FILE_PATH]
生成报告等。
Fixture
多个用例需要使用相同的数据,每次都在用例中准备显得很重复麻烦,这时候,可以使用 Fixture 来构建用例,使多个用例共用相同的数据对象配置。
使用 Fiture 第一部是定义一个继承自::testing::Test
的类,在类中定义初始化函数,清理函数和声明需要使用的对象。
class QueueTest : public ::testing::Test { // 定义套件名,继承自 Test
protected: // 建议,子类可用成员
//定义setup 函数,在每个用例执行前调用
void SetUp() override {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
// 定义清理函数,在每个用例执行后调用
// void TearDown() override {}
// 定义需要用到的变量
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};
//写用例,套件名(上面定义的类名),用例名
TEST_F(QueueTest, IsEmptyInitially) {
EXPECT_EQ(q0_.size(), 0); //直接使用成员变量
}
以上我们定义了一个套件 QueueTest , 当我们执行该套件用例时,
- gtest 构建 QueueTest 实例 qt1;
- 调用 qt1.SetUp() 初始化
- 执行一个用例
- 调用 qt1.TearDown() 清理
- 析构 qt1 对象
- 回到1,执行下一个用例
从步骤可知,不同用例之间,数据实际都是独占的,不会相互影响。
使用 fixture 编写用例后,同单独测试用例 TEST 一样,需要编写 main ,然后编译连接,执行测试。
使用 gmock
gmock 现在已经和入 gtest 的代码库, 1.8 和之后的版本直接在 gtest github 主页中获取,低版本仍然在原 github主页。
gmock 需要依赖 gtest 使用,在测试中,当我们测试的对象需要依赖其他模块、接口,但是往往受条件限制无法使用真实依赖的对象,通过 mock 对象来模拟我们需要依赖,以协助测试本模块,mock 对象具有和真实对象一样的接口,但是我们可以在运行时指定他的行为,如何被使用,使用多少次、参数,使用时返回什么等。
编译
编译说明
gmock 编译需要依赖 gtest, 准备好 gtest 和 gmock (同一个版本)后,手动编译的方法如下:
设置好 gtest 和 gmock 的工程路径,或者在下面命令中直接替换源路径。
g++ -isystem ${GTEST_DIR}/include -I${GTEST_DIR} \
-isystem ${GMOCK_DIR}/include -I${GMOCK_DIR} \
-pthread -c ${GTEST_DIR}/src/gtest-all.cc
g++ -isystem ${GTEST_DIR}/include -I${GTEST_DIR} \
-isystem ${GMOCK_DIR}/include -I${GMOCK_DIR} \
-pthread -c ${GMOCK_DIR}/src/gmock-all.cc
ar -rv libgmock.a gtest-all.o gmock-all.o
由命令可知,libgmock.a 包含了 libgtest.a,所有实际编译测试程序时,只需要链接 libglmock.a 就好了。
使用 cmake编译库,进入 gmock 目录(此处 gtest 已经准备并且与 gmock 同级目录)
$ cd ./googlemock/; mkdir build
$ cd ./build; cmake ..
$ make
生成 libgmock.a 库在 build 目录下, 同时生成 libgtest.a gtest/
下, 与上面手动编译把 gtest 和 gmock 打在一个 libgmock.a 不同,使用这种编译程序需要同时指定 链接 libgmock.a 和 libgtest.a, 否则会报各种 undefine 的错误 。
编译测试程序 :
g++ -isystem ${GTEST_DIR}/include \
-isystem ${GMOCK_DIR}/include \
-pthread path/to/your_test.cc libgmock.a -o your_test
测试时,我链接 cmake 编译出来的库时报错,查看库中很多符号没有,原因就是 cmake 输出的 libmock.a 不包含 gtest,需要指定链接 libgtest.a
gmock 测试程序
gmock mock 对象,可以定义函数期望行为,如被调用时返回的值,期望被调用的次数,参数等,如果不满足就会报错。
定义 gmock 对象的基本步骤:
- 创建 mock 对象继承自原对象,并用框架提供的宏
MOCK_METHODn(); (or MOCK_CONST_METHODn();
描述需要模拟的接口 - 写用例,在用例中使用宏定义期望接口的行为,如果定义的行为执行用例时不满足,就会报错
借用主页提供的例子改写,简单学习下如何使用 mock
比如你测试的对象依赖的接口定义如下,
class Turtle {
public:
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;
virtual int GetX() const = 0;
virtual int GetY() const = 0;
};
此时通过继承这个对象,定义了 mock 对象,在对象中通过宏描述需要 mock 的接口,这样,就完成了对象的 mock 操作。
#include "gmock/gmock.h"
#include "gtest/gtest.h
class MockTurtle: public Turtle {
public:
// MOCK_METHOD[参数个数](接口名,接口定义格式);
MOCK_METHOD0(PenUp, void());
MOCK_METHOD0(PenDown, void());
MOCK_METHOD1(Forward, void(int distance));
MOCK_METHOD1(Turn, void(int degrees));
MOCK_METHOD2(GoTo, void(int x, int y));
MOCK_CONST_METHOD0(GetX, int());
MOCK_CONST_METHOD0(GetY, int());
};
定义了 mock 对象后,就可以在测试用例使用 mock 对象替代原依赖对象,执行测试了。
using ::testing::AtLeast;
TEST(PainterTest, PenDownCall) {
MockTurtle turtle;
EXPECT_CALL(turtle, PenDown())
┊ .Times(AtLeast(2));
// 期望这个函数在本次测试需要至少被调用2次
// 否则报错
turtle.PenDown();
turtle.PenDown();
}
using ::testing::Return;
TEST(PainterTest, GetX) {
MockTurtle turtle;
EXPECT_CALL(turtle, GetX())
┊ .Times(4)
┊ .WillOnce(Return(100))
┊ .WillOnce(Return(150))
┊ .WillRepeatedly(Return(200));
// 期望这个函数在本次测试需要被调用4次
// 否则报错
// 第一次调用返回100, 第二次150,之后都是200
EXPECT_EQ(turtle.GetX(), 100);
EXPECT_EQ(turtle.GetX(), 150);
EXPECT_EQ(turtle.GetX(), 200);
EXPECT_EQ(turtle.GetX(), 200);
}
using ::testing::_;
TEST(PainterTest, GoTo) {
MockTurtle turtle;
EXPECT_CALL(turtle, GoTo(_, 100));
// 期望调用参数,第一个任意,第一个必须为 100
turtle.GoTo(1, 100);
EXPECT_CALL(turtle, GoTo(_, 101));
turtle.GoTo(2, 101);
}
gmock 使用宏设置期望是粘性的,意思是当我们调用达到期望后,这些设置的期望仍然保持活性。
举个例子,mock 一个接口 a(int),我们设置第一个期望: a 调用传入参数任意,调用次数任意;然后设置第二个期望: a 调用传入参数必须为1, 调用次数为2;当我们调用 a(1) 两次后,达到了第二个期望上边界(此时第二个期望并不会失效),这时候,第三次调用 a(1) 就会报错,因为匹配到第二个期望说调用超过2次。(总是匹配最后一个期望)
如果想设置多个期望,并按顺序执行,可以如下实现
//sticky
TEST(PainterTest, GetY) {
//设置调用按照期望设置顺序,定义一个 sq 对象,名随意
using ::testing::InSequence;
InSequence dummyObj;
MockTurtle turtle;
EXPECT_CALL(turtle, GetY())
┊ .Times(2)
┊ .WillOnce(Return(100))
┊ .WillOnce(Return(150))
┊ .RetiresOnSaturation(); // 指定匹配后不再生效,退休
EXPECT_CALL(turtle, GetY())
┊ .Times(1)
┊ .WillOnce(Return(200))
┊ .RetiresOnSaturation();
EXPECT_EQ(turtle.GetY(), 100);
EXPECT_EQ(turtle.GetY(), 150);
EXPECT_EQ(turtle.GetY(), 200);
}
最后,和 gtest 中一样,可以自己编写 main 函数完成调用,不过注意到,调用的 init 函数不同,之后便可以按前面提到的编译命令执行编译,运行测试了。
int main(int argc, char** argv) {
//初始化 gtest 和 gmock
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}
参考
我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=38q7yly61twk8