工具癖程序员

Google 单元测试框架

2018-09-04  本文已影响51人  orientlu

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 断言提供两个版本

完整的 宏定义, 或见源码 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

上一篇下一篇

猜你喜欢

热点阅读