一、安装googletest
单测对程序员而言是提升代码质量最重要、最有效的一个措施,对程序员来说,要想写一个好的程序,那么必定少不了好的单元测试。googletest(gtest)是google开发出来的一个开源的、跨平台的测试框架,是C++中最出名的测试框架。
gtest支持linux、windows以及mac系统,安装它依赖下面几项:
- gtest源码:gtest属于开源项目,代码仓库https://github.com/google/googletest。
- cmake:gtest使用cmake构建项目,编译需要cmake环境,cmake下载地址。
- 编译器: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();
}
执行结果:
三、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);
}
测试过程可以描述为:
- 创建一个栈对象的实例s。
- 推入3个元素,以便后面pop使用。
- 开始测试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
,其中的SetUp
和TearDown
函数分别是初始化和清理函数,也就是类生成前和使用后要做的工作。
此时使用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})
此处评论已关闭