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 , 当我们执行该套件用例时,

  1. gtest 构建 QueueTest 实例 qt1;
  2. 调用 qt1.SetUp() 初始化
  3. 执行一个用例
  4. 调用 qt1.TearDown() 清理
  5. 析构 qt1 对象
  6. 回到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.alibgtest.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 编程指导codebook

gmock mock 对象,可以定义函数期望行为,如被调用时返回的值,期望被调用的次数,参数等,如果不满足就会报错。
定义 gmock 对象的基本步骤:

  1. 创建 mock 对象继承自原对象,并用框架提供的宏 MOCK_METHODn(); (or MOCK_CONST_METHODn(); 描述需要模拟的接口
  2. 写用例,在用例中使用宏定义期望接口的行为,如果定义的行为执行用例时不满足,就会报错

借用主页提供的例子改写,简单学习下如何使用 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,799评论 25 707
  • 用了大约20分钟,完成了课后21天加强训练的第一张思维导图,规划了一周的主要行程,也不知道这样用的对不对,忐忑一下!
    李大鹏_365阅读 207评论 0 0
  • 日常脑子里有很多念头,都是一闪而过,一些好的点,就随手记在了手机上。以下是我最近是一段时间,有关于广告行业的小思考...
    贾桃阅读 813评论 0 2
  • 最完美的婚姻一定是势均力敌的。 ​​​ “我养你”,才是世界上最毒的情话 这两天心情一直不算好,于是刷遍了网络综艺...
    欧阳茜茜阅读 495评论 0 1