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.ccg++ -isystem ${GTEST_DIR}/include -I${GTEST_DIR} \ -isystem ${GMOCK_DIR}/include -I${GMOCK_DIR} \ -pthread -c ${GMOCK_DIR}/src/gmock-all.ccar -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.hclass 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