专栏首页orientluGoogle 单元测试框架

Google 单元测试框架

使用 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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • FreeRTOS 任务调度 任务切换

    前面文章 < FreeRTOS 任务调度 任务创建 > 介绍了 FreeRTOS 中如何创建任务以及其具体实现。 一般来说, 我们会在程序开始先创建若干个任务...

    orientlu
  • shell 当前工作目录的绝对路径

    问题就出在最后那句,本意我是希望把/home/lcd/something 复制到我脚本的执行目录。 假设我的脚本目录在/home/lcd/shell/下,

    orientlu
  • std::sort coredump 说起

    c++ 标准库 sort() 默认采用 < 这个 operator 来排序的, 另个一个重载函数增加第三个参数,指定一个比较的函数,函数接受两个参数。 对于基...

    orientlu
  • Guava 指南 之「前置条件」

    前置条件 Guava 提供了很多用于进行前置条件检查的工具,我们强烈建议静态导入这些方法。 每个方法都用三种形式: 没有额外的参数。抛出的任何异常都没有错误信息...

    CG国斌
  • LOJ#6283. 数列分块入门 7

    内存限制:256 MiB时间限制:500 ms标准输入输出 题目类型:传统评测方式:文本比较 上传者: hzwer 提交提交记录统计讨论测试数据 题目描述 给出...

    attack
  • Android数据库高手秘籍(六)——LitePal的修改和删除操作

    在上一篇文章中,我们学会了使用LitePal进行存储数据的功能。确实,比起直接使用Android原生的API,LitePal明显简单方便了太多。那么,在增删改查...

    用户1158055
  • if exists table drop table ORACLE 存储过程

    CREATE OR REPLACE PROCEDURE DROPEXITSTABS (TAB_NAME_IN IN varchar2) IS v_...

    用户5640963
  • 每日一题(字符串分割)

    编程是很多偏计算机、人工智能领域必须掌握的一项技能,此编程能力在学习和工作中起着重要的作用。因此小白决定开辟一个新的板块“每日一题”,通过每天一道编程题目来强化...

    小白学视觉
  • 三种排序方式

    冒泡就像鱼吐得泡泡一样,泡泡越来越大,连起来看就是小泡泡在下面,大泡泡在上面。联想到数字就是大的数字在上面,小的数在下面。给你一个串数字,根据冒泡排序的方法演示...

    微醺
  • Uber Eats计划使用无人机送外卖

    Uber Eats已经是最受欢迎的外卖应用程序之一,但本周在日本的Uber Elevate亚太博览会上,优步公布了一种新的交付方式:无人机。

    AiTechYun

扫码关注云+社区

领取腾讯云代金券