单元测试框架的开发
对测试的理解
- 通过测试 = 概率性正确 != 没有BUG
- 要注意编码规范,比如某些标准库的禁止使用、C++中的避免直接使用using namespace std .
认识Google Test框架
下载
Github地址:Google Test
本例使用Ubuntu 18.04
环境,执行命令
git clone https://github.com/google/googletest
编译GoogleTest
首先,新建一个文件夹名为/build
并进入该目录。
mkdir build
cd build
执行命令cmake ../
,cmake利用CMakeLists.txt中的规则生成Makefile文件,当前build目录下将会产生构建测试框架的内容文件,包括Makefile文件。接着直接执行make命令即可对googletest项目进行编译。若cmake阶段报错,首先需要确保安装了cmake。
安装cmake,然后输入root密码(Linux输入密码默认不回显,输完后直接回车即可)
sudo apt install cmake
检验是否安装成功
cmake --version
然后执行刚刚的步骤(cmake …/ 以及 make)即可。若cmake仍然有问题,可以在googletest/CMakeLists.txt
中添加一行语句:add_definitions(-std=c++11)
,使其执行C++11的标准即可。
安装成功:
测试用例
要使用gtest来进行测试,需要先将googletest目录下的/include
和/lib
目录拷贝到自己的项目中,因为它们分别包含了gtest的声明和定义文件。
cp googletest/build/lib/* lib/
cp -R googletest/googletest/include/* include/
使用如下测试用例:
#include <gtest/gtest.h>
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
TEST(test,add) {
EXPECT_EQ(add(3,4),7);
EXPECT_NE(add(3,4),8);
EXPECT_GT(add(3,4),6);
EXPECT_LT(add(3,4),8);
EXPECT_GE(add(3,4),7);
EXPECT_LE(add(3,4),7);
}
int main(int argc, char *argv[]) {
testing::InitGoogleTest(&argc,argv);
return RUN_ALL_TESTS();
}
编译测试项目
g++ -I./include -L./lib test.cpp -lgtest
-I
用来增加头文件的检索路径-L
用来增加库文件的检索文件-lgtest
链接库,实际查找libgtest.a,位于lib目录下
如果在编译时出错,可以尝试这条命令:
g++ -std=c++11 test.cpp -I./include -L./lib -lgtest -lpthread
-std=c++11
使用c++11标准编译-lpthread
加上线程库
GoogleTest效果展示
疑问
- TEST是如何进行测试的?
- RUN_ALL_TESTS()是如何知道我们定义了哪些TEST的呢?
模仿gtest开发一个测试框架
基本结构
- 使用宏定义手段实现单元测试,首先根据流程,初步搭建一个测试框架
/*mytest.h*/
#ifndef _MYTEST_H
#define _MYTEST_H
#define EXPECT(a, comp, b) { \
if (!((a) comp (b))) printf("error\n"); \
}
//减少代码冗余
#define EXPECT_EQ(a, b) EXPECT(a, ==, b)
#define EXPECT_NE(a, b) EXPECT(a, !=, b)
#define EXPECT_GT(a, b) EXPECT(a, >, b)
#define EXPECT_GE(a, b) EXPECT(a, >=, b)
#define EXPECT_LT(a, b) EXPECT(a, <, b)
#define EXPECT_LE(a, b) EXPECT(a, <=, b)
#define TEST(a, b) void a##_##b()
int RUN_ALL_TESTS() {
// to do something
return 0;
}
#endif
一个小技巧
-
设置函数属性,让其成为构造函数,先于主函数运行.
__attribute__((constructor)) //放在目标函数定义之前
RUN_ALL_TESTS()功能实现
- 利用上述__attribute__关键字技巧,使得TEST宏不仅能够扩充为对应的函数名,还能扩充为一整个功能函数,attribute使其能够在主函数之前运行后将结果存到某一存储区,那么RUN_ALL_TEST就可以遍历该存储区来对所有的测试用例进行测试了。
#define TEST(a, b) \
void a##_##b(); \
__attribute__((constructor)) \
void register_##a##_##b() { \
add_test(a##_##b, #a "." #b); \
} \
void a##_##b()
//扩展为功能函数,不要忘了函数声明。该函数将测试结果存到存储区中。
struct TestData {
void (*func)();
char *func_name;
} func_arr[100];
int func_cnt = 0;
//定义存储区的格式,工程中不建议与宏定义和声明放在同一文件中
void add_test(void (*func)(), const char *func_name) {
func_arr[func_cnt].func = func;
func_arr[func_cnt].func_name = strdup(func_name);
func_cnt += 1;
return ;
}
//将test添加到存储区
int RUN_ALL_TESTS() {
for (int i = 0; i < func_cnt; i++) {
printf("[ RUN ] %s\n", func_arr[i].func_name);
func_arr[i].func();
}
return 0;
}
//批量遍历test输出结果(简易版本)
初步效果
使用如下的测试文件:
初步的mytest.h实现:
编译后的运行效果:
由上图可见,已经展示出了初步的效果。
测试框架进阶美化
改变部分字体颜色
gnu规则下改变字体颜色方法
终端的字符颜色是用转义序列控制的,是文本模式下的系统显示功能,和具体的语言无关。
转义序列是以ESC开头,即用\033
来完成(ESC的ASCII码用十进制表示是27,用八进制表示就是033)。
书写格式:
开头部分:\033[显示方式;前景色;背景色m + 结尾部分:\033[0m
注意:开头部分的三个参数:显示方式,前景色,背景色是可选参数,可以只写其中的某一个;另外由于表示三个参数不同含义的数值都是唯一的没有重复的,所以三个参数的书写先后顺序没有固定要求,系统都能识别;但是,建议按照默认的格式规范书写。
对于结尾部分,其实也可以省略,但是为了书写规范,建议\033[***开头,\033[0m结尾
。
数值表示的参数含义:
显示方式: 0(默认值)、1(高亮)、22(非粗体)、4(下划线)、24(非下划线)、 5(闪烁)、25(非闪烁)、7(反显)、27(非反显)前景色: 30(黑色)、31(红色)、32(绿色)、 33(黄色)、34(蓝色)、35(洋 红)、36(青色)、37(白色)背景色: 40(黑色)、41(红色)、42(绿色)、 43(黄色)、44(蓝色)、45(洋 红)、46(青色)、47(白色)
常见开头格式:
\033[0m 默认字体正常显示,不高亮
\033[31;0m 红色字体正常显示
\033[1;32;40m 显示方式: 高亮 字体前景色:绿色 背景色:黑色
\033[0;31;46m 显示方式: 正常 字体前景色:红色 背景色:青色
在该例中,我们要将[=========]
改为绿色,只需要这样修改:
printf("\033[1;32m[=========]\033[0m running %d test cases\n", func_cnt);
//局部改变颜色都以\033[0m结束,只有[ , 没有 ].
但是,反复如此书写代码较为繁琐,所以可以使用宏定义来改进:
对需要改变颜色的内容,只需将其作为msg参数传入即可:
效果如下:
提示错误信息
将测试不通过的文件、行号输出在屏幕上,并且输出预期的值。
新引入一个expect_printf函数,将FILE和LINE的宏作为参数传入,下面是expect_printf的定义:
输出总结语句
- 若测试运行完毕后没有发生错误,则输出OK,否则输出Failed,并且输出执行时间。
- 只需设置一个全局标志err_exist,初始化为0,然后每次调用expect_printf函数(即发生错误时),将err_exist设置为1;
- 在RUN_ALL_TESTS中,每一次遍历测试函数时都将err_exist初始化为0,执行完测试函数后,根据err_exist的值作为条件输出不同的总结语句。
- 记录测试函数运行的时间,用到了ctime,需要include一下,然后定义两个clock_t类型的变量startTime和endTime,运行时调用clock()函数,将返回值作为开始时间赋给startTime,结束时间同理。
- RUN_ALL_TESTS完整代码实现如下:
最终效果展示
一个简单又美观的单元测试框架就完成了!是不是比GoogleTest还要美观一些呢?