一、安装googletest

单测对程序员而言是提升代码质量最重要、最有效的一个措施,对程序员来说,要想写一个好的程序,那么必定少不了好的单元测试。googletest(gtest)是google开发出来的一个开源的、跨平台的测试框架,是C++中最出名的测试框架。

gtest支持linux、windows以及mac系统,安装它依赖下面几项:

  1. gtest源码:gtest属于开源项目,代码仓库https://github.com/google/googletest
  2. cmake:gtest使用cmake构建项目,编译需要cmake环境,cmake下载地址
  3. 编译器:linux环境下可使用g++编译,windows环境下使用vs或者clion等工具编译,mac环境使用xcode或clion等工具编译。

这里的测试环境为mac+clion(付费),clion下载地址,选择clion是因为clion跨平台。

windows环境除了配置clion编译环境以外,其他步骤和mac系统一致。

1.1 环境配置

第一步,先使用git克隆代码到本地,注意最好不要放到中文路径了。

git clone https://github.com/google/googletest.git

第二步,安装cmake,不同系统的的安装方式不一样,windows在上面的页面下载一直下一步安装就行了,其他系统可以直接使用对应平台的包管理工具安装。

# mac
brew install cmake
# centos 
yum install cmake
# ubuntu
apt-get install cmake

第三步,安装clion,linux和mac环境下安装clion和gcc环境就可以使用了,windows配置clion编译环境可参考Window10上CLion极简配置教程

1.2 编译gtest库

配置好环境后,使用clion打开代码目录,然后载入代码目录,选择gtest项目编译生成:

编译成功后生成libgtestd.a文件到cmake编译路径的lib路径下:

生成的libgtestd.a即为gtest的库文件,项目中引用这个库文件就能使用gtest了。

二、使用googletest

2.1 引入库

将libgtestd.a文件拷贝到代码根路径的lib路径下,在CMakeList.txt中加上以下内容:

# 添加上库文件的路径,注意相对路径
link_directories(lib/)
# 添加可执行文件
add_executable(demo demo.cpp)
# 链接gtest库文件
target_link_libraries(demo libgtestd.a)

2.2 引入头文件

拷贝googletest/include目录下的gtest目录到当前目录下,然后在CMakeList.txt中添加上对应的调用:

include_directories(
    include/
)

然后在代码中添加头文件gtest/gtest.h就可以使用了。

2.3 测试

添加代码add.cpp

#include "gtest/gtest.h"

int add(int a, int b) {
    return a + b;
}

TEST(add, zero) {
    EXPECT_EQ(0, add(0, 0));
}

TEST(add, positive_number) {
    EXPECT_EQ(3, add(1, 2));
}

TEST(add, negative_number) {
    EXPECT_EQ(-3, add(-1, -2));
}

int main() {
    ::testing::InitGoogleTest();
    return RUN_ALL_TESTS();
}

执行结果:

 title=

三、gtest的使用教程

参考文档:Googletest Primer,google官方出品。

3.1 基本用法

gtest最基本的用法就是断言,它内部提供了很多种断言方式,例如:

ASSERT_EQ()
ASSERT_TURE()
EXPECT_EQ()
EXPECT_TRUE()
// ...

其中ASSERT_*的断言,在条件不满足后会终止,而EXPECT_*不会终止。

以上面的代码为例,代码编写了一个add函数,返回两个传参的和:

int add(int a, int b) {
    return a + b;
}

然后引入gtest并写了三个测试用例:

#include "gtest/gtest.h"

TEST(add, zero) {
    EXPECT_EQ(0, add(0, 0));
}

TEST(add, positive_number) {
    EXPECT_EQ(3, add(1, 2));
}

TEST(add, negative_number) {
    EXPECT_EQ(-3, add(-1, -2));
}

三个用例分别表示:

  • 测试零值相加
  • 测试正数相加
  • 测试负数相加

主函数中添加启动gtest的入口:

::testing::InitGoogleTest();
RUN_ALL_TESTS();

运行程序,系统就会自动调用三个测试用例的函数来测试,并输出测试报告:

...
[ RUN      ] add.zero
[       OK ] add.zero (0 ms)
[ RUN      ] add.positive_number
[       OK ] add.positive_number (0 ms)
[ RUN      ] add.negative_number
[       OK ] add.negative_number (0 ms)
...

如果中间有断言失败的地方,报告也会表达出来。例如修改上面测试中的负数相加函数来制造错误场景:

TEST(add, negative_number) {
    EXPECT_EQ(-3, add(-1, 2)); // 把-2改成2,制造错误场景。
}

再执行测试,报告中就会把不通过的案例展示出来,并且会定位到对应的行,打印出失败的详细原因:

当案例执行失败后,我们也可以打印出一些我们自己的数据以供调试使用,例如:

TEST(add, negative_number) {
    EXPECT_EQ(-3, add(-1, 2)) << "this is a incorrect test";
}

案例失败后,不仅会打印出失败原因,还会打印出我们自己添加的语句:

3.2 常用断言

基本断言

致命断言非致命断言验证
ASSERT_TRUE(condition);EXPECT_TRUE(condition);条件condition为真
ASSERT_FALSE(condition);EXPECT_FALSE(condition);条件condition为假

二进制比较

致命断言非致命断言验证
ASSERT_EQ(val1, val2);EXPECT_EQ(val1, val2);val1 == val2
ASSERT_NE(val1, val2);EXPECT_NE(val1, val2);val1 != val2
ASSERT_LT(val1, val2);EXPECT_LT(val1, val2);val1 < val2
ASSERT_LE(val1, val2);EXPECT_LE(val1, val2);val1 <= val2
ASSERT_GT(val1, val2);EXPECT_GT(val1, val2);val1 > val2
ASSERT_GE(val1, val2);EXPECT_GE(val1, val2);val1 >= val2

字符串比较

致命断言非致命断言验证
ASSERT_STREQ(str1,str2);EXPECT_STREQ(str1,str2);两个c字符串内容相同
ASSERT_STRNE(str1,str2);EXPECT_STRNE(str1,str2);两个c字符串内容不同
ASSERT_STRCASEEQ(str1,str2);EXPECT_STRCASEEQ(str1,str2);两个c字符串内容相同(忽略大小写)
ASSERT_STRCASENE(str1,str2);EXPECT_STRCASENE(str1,str2);两个c字符串内容不同(忽略大小写)

四、使用Test Fixtures

Test Fixtures使用场景:测试案例需要初始化数据或者多个测试案例使用相同的测试数据。

例如在对一个的做单元测试时,测试pop功能,按照上面的测试方法,测试案例得这么写:

TEST(stack, pop) {
  my_stack s;
  // 先推入3个元素
  s.push(1); s.push(2); s.push(3);
  // 测试pop
  s.pop();
  EXPECT_EQ(s.size(), 2);
}

测试过程可以描述为:

  1. 创建一个栈对象的实例s。
  2. 推入3个元素,以便后面pop使用。
  3. 开始测试pop。

从直观上来看,所有和pop相关的测试案例都要这么来写,要先推入元素,再弹出。而实际上,步骤1和步骤2是和本轮测试无关,它只起到了初始化数据的作用,它是多余的,但是所有的测试案例又不得不做这一步操作。那么有没有办法解决这个问题呢?有!Test Fixtures的就是解决这种问题的,它可以在测试案例开始前自动生成好需要的数据。

定义了一个简单的的类:

class my_stack {
public:
    void push(int a) {
        s.push(a);
    }
    void pop() {
        s.pop();
    }
    int size() {
        return s.size();
    }
private:
    stack<int> s;
};

再定义一个测试类stack_test

class stack_test : public ::testing::Test {
protected:
    void SetUp() override {
        stack1.push(1);
        stack1.push(2);
    }

    void TearDown() override {
    }

    my_stack stack1;
    my_stack stack2;
};

它要公有继承于::testing::Test,其中的SetUpTearDown函数分别是初始化和清理函数,也就是类生成前和使用后要做的工作。

此时使用TEST_F宏定义来测试:

TEST_F(stack_test, push) {
    EXPECT_EQ(stack1.size(), 2);
    EXPECT_EQ(stack2.size(), 0);
    stack2.push(1);
    EXPECT_EQ(stack2.size(), 1);
}

TEST_F(stack_test, pop) {
    EXPECT_EQ(stack1.size(), 2);
    EXPECT_EQ(stack2.size(), 0);
    stack1.pop();
    EXPECT_EQ(stack1.size(), 1);
}

在执行TEST_F之前,gtest会自动构建一个stack_test的实例,并执行SetUp函数。也就是说,当真正执行到我们的测试代码的时候,就已经存在一个初始化好的测试环境了。这个时候可以直接访问stack_test的内部成员,通过成员变量来做单元测试。

原理来看其实很简单,就是把初始化的过程交给了gtest来完成,它来帮我们实例对象,进行初始化,我们直接用就行。

测试结果:

五、其他

5.1 clion环境跨平台使用gtest

如何在不改变CMakeList.txt的情况下跨平台使用gtest?配置CMakeList,根据不同平台读取不同的库:

# 根据不同平台设置库的目录
if (UNIX AND NOT APPLE) # unix非苹果系统
    set(PROJ_LIBRARY_PATH /home/maqian/code/lib)
    set(GTEST_LIBRARY_NAME libgtestd.a)
elseif (WIN32) # windwos系统
    set(PROJ_LIBRARY_PATH d:/mingw64/lib)
    set(GTEST_LIBRARY_NAME libgtestd.a)
elseif (APPLE) # mac系统
    set(PROJ_LIBRARY_PATH /Users/maqian/code/lib)
    set(GTEST_LIBRARY_NAME libgtestd.a)
else () # 未知系统
    MESSAGE(STATUS "other platform: ${CMAKE_SYSTEM_NAME}")
endif ()
# 引入链接库目录
link_directories(${PROJ_LIBRARY_PATH})
# 链接库到目标
target_link_libraries(xxxx ${GTEST_LIBRARY_NAME})

六、参考

Google Test support

Googletest Primer

最后修改:2020 年 02 月 08 日
如果觉得我的文章对你有用,请随意赞赏