前言
一、单元测试框架
1.GTest安装
二、单元测试插桩
1.插桩原理
2.插桩实现
3. 插桩实例
三、单元测试度量
1.正确率
2.覆盖率
四、未尽事宜
总结
前言
?????????一个软件面世应该经历哪些环节呢?简要来说,要经过概念、计划、设计、开发、测试、发布。其中测试又分为:单元测试、集成测试、系统测试、生产测试。如果有硬件还要经历中试、小批量生产测试,可见测试也不是一个简单的工作,也是蛮复杂的。 ????因为我是做开发的,单元测试跟开发结合最紧密,是开发者必须具备的基本素质之一。但现实情况下,哪个公司又会专门配备单元测试人员呢?很多都是为了CI里面的指标应付罢了,而这一块知识仍然是不可或缺的,下面就是我从最基础开始 ????介绍,一直到打桩、度量等高阶应用会全部涉及到,读了这篇文章,你i基本就对单元测试了如指掌了,只是缺少case-ba-case的经验而已了。
一、单元测试框架
????????我用过很多单元测试框架,其中最推荐的就是Google的GTest,这款开源框架包括了两部分,googletest和googlemock,在这个地址下面下载一个包,就都包括了。曾经对gmock研究了一段时间,发现非常不好用,mock的方法也不好理解,而且有限制,于是就放弃了,只用gtest框架,插桩功能 自己开发!!
1.GTest安装
登陆github下载源码压缩包:
https://github.com/google/googletest/tree/main
解压缩:
tar -xvf googletest-release-1.11.0.tar.gz
cd googletest-release-1.11.0/
执行命令:
cmake CMakeLists.txt
执行成功如下图:
然后编译执行:make
?然后到lib目录下,可以看到已经生成了静态库:
最后 安装,其实就是把库和头文件拷贝到系统下:
sudo cp libgtest*.a /usr/lib
sudo cp –a include/gtest /usr/include
这样就安装完毕了!
2.Demo验证
我们写一个函数,看看安装的gtest有没有问题
#include <stdio.h>
#include <gtest/gtest.h>
int add(int a, int b)
{
return a + b;
}
TEST(testcase, test0)
{
EXPECT_EQ(add(2,3), 5);
}
int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
编译这个程序:
g++ gt.c -lgtest -o gt
./gt
运行结果如下:
?????????这只是一个Hello World级别的Demo,表明GTest现在是Work的。其中GTest提供了大量的宏定义去Assert你的结果,最终以报告形式展现给你。如果要了解更多GTest的Assert的用法,可以专门搜索这方面的文章。
二、单元测试插桩
? ? ? ? 为什么要插桩呢?如果开发的软件是运行在设备上要访问硬件,或者在网络上要访问远程接口,或者在服务器上要访问数据库,而实际情况不具备这些条件,为了快速验证,就需要在调用外部功能的时候进行插桩,让调用流程跑到桩函数,返回一个预先设好的值,来让上层的业务流程继续运转下去,而不至于在这个地方崩掉或者阻塞。
? ? ? ? 当然如果想屏蔽一些资源调用,也有其他办法,比如在产品代码中加一个宏定义来区分是调用正式接口还是桩接口,但是对于代码洁癖的我来说,我只是想测试一下产品代码,为什么要去修改他呢,修改就意味着风险。即要测试它,又要不修改它,于是就有了插桩类似于Hook的方式,代码结构可以按照如下设计组织
1.插桩原理
? ? ? ? 程序便后load到内存里面,这些汇编指令全都在代码段,按照编译器的规则排布好的。对于函数跳转一版都是jump指令+函数地址。我们找到这条指令,然后把jump指令后的函数地址换成桩函数地址,然后程序执行的时候就可以跳转到装函数了,见下图本来要跳转到绿色区域,但是替换函数地址后就会跳转到黄色区域了,这是最基本的原理
2.插桩实现
? ? ? ?本实现接口简洁只有两个接口:
unsigned int Stub_Install(void *dest_func, void *stub_func)
unsigned int Stub_Remove(void *dest_func, void *stub_func)
???????? 本实现使用了链表,对桩进行管理,程序运行期间,可以实现动态的不断的插桩、删桩去测试,下面给出主要的实现代码及步骤
3. 插桩实例
????????已将插桩功能做成了库,所以在实例里面只需要包含有文件调用接口就可以了,代码实现如下:
#include <stdio.h>
#include "stub.h"
int Add(int a, int b)
{
return a + b;
}
int Add_Stub(int a, int b)
{
printf("I am Stub!\n");
return 0;
}
int main(int argc, char **argv)
{
int sum = 0;
unsigned int RetValue = STUB_ERR_SUCCESS;
RetValue = Stub_Install(Add, Add_Stub);
if (STUB_ERR_SUCCESS == RetValue)
{
sum = Add(1, 2);
printf("sum = %d\n", sum);
Stub_Remove(Add, Add_Stub);
}
return 0;
}
运行结果:
I am Stub
sum = 0
显然跳过了真正的add函数
三、单元测试度量
衡量单元测试工作做得质量如何,有两个指标可以衡量。单元测试的覆盖率和正确率。
覆盖率:覆盖率统计有专门的工具可以统计出来,统计方法有函数覆盖率,分支覆盖率、行覆盖率,这些覆盖率实现的难易程度也是按照这个顺序递增的
正确率:测试用例PASS OK的除以总的用例数就是正确率。这个就是Gtest给出的数据。
1.正确率
Gtest提供的正确率:Gtest运行完测试用例后,打印的最后一节里面,有两个数,红色圈的除以黄色圈的就是通过率,下图通过率是100%
2.覆盖率
如果想把覆盖率展现出来,需要使用一系列的工具:
在编译命令参数中增加:-fprofile-arcs -ftest-coverage?
????????这个是产生gcno和gcda文件的,这些文件记录了整个代码的符号,分支,行数等所有信息,编译时产生gcno文件,每个源文件一个,运行时才产生gcda文件,也是每个源文件一个
使用lcov工具:
? ? ? ? 这个工具是产生覆盖率信息的,而且参数灵活,可以剔除某些目录,覆盖率信息合并等功能,命令使用样例:
lcov -c -o ut_agency.info -d . -d ../src/ -d ../src/agency/
lcov --remove ut_agency.info '*c++*' -o ut_agency.info
lcov --remove ut_agency.info '*gtest*' -o ut_agency.info
genhtml产生网页文件
该命令将在build目录下产生一个网页site
genhtml -o build ut_agency.info
此时可以双击index.html
?
非常直观,非常好看吧,而且点击directory那一列,还支持代码级浏览,红色部分是没有覆盖到的测试分支,浅蓝色区域是已经覆盖到的,非常容易找到自己还有哪部分代码没有做单元测试。
打包工具:zip
? ? ? ? zip是一个打包工具,你是不是想把单元测试集成到你的CI里面,每天自动将结果发送到相关人员的邮箱?那么就用zip打包成一个压缩包发送吧?
四、未尽事宜
? ? ? ? 现在已经把单元测试的相关内容基本上都讲到了,那么就可以大张旗鼓搞单元测试了吗?是可以开始搞了,不过你可能做了一段时间后,开始烦恼不断。因为单元测试用例越来越多,随之而来的问题就是:
单元测试用例多了后如何管理?
单元测试用例想管理好,那要事先进行设计
要设计单元测试用例,要先熟悉正式产品代码
想更好的理解产品代码,要先熟悉产品需求
????????所以,单元测试代码要按照正式的代码规范去写,去管理,而且要不断的投入维护,才能跟正式产品一直走下去。有的产品做到最后把测试代码全扔了页屡见不鲜。
总结
? ? ? ? 本文讲的是技术,单元测试其实还涉及到项目管理,项目策略,如果去做它。这些不在本文讨论范围内,如果想了解可以留言交流哦
|