目录
单元测试概念
引子
定义
内容
方法
单元测试模型
测试模型构建
单元测试工具简介
Cmockery使用介绍
简介
使用
VPBX实践
UT框架搭建
目录
编译:
实例demo
例1:
例2:
例3:
例4:
例5:
例6:
例7:
遗留问题
-
单元测试概念
-
引子
-
一种观点:“实际工作中,写好程序后对程序功能的调试就是一种单元测试”,“写好程序,编译完,跑一跑,看看写得对不对,这就是最简单的UT啊!”。是的这些是简单的UT,但是这些并不能保证你的程序完整性和正确性,只能保证你在进行了“简单的UT”后的流程正确而已。试想,如果在做系统测试时,测试的同事也随心所欲的测测,“简单的ST”一下,说这个产品没问题,谁相信?我想我们自己都不会相信测试同事给出的结果,因此测试部的同事为了使大家相信我们产品的质量,或者他们对产品测试的结果,通常会先设计大量的测试用例,然后通过执行这些测试用例,最后得到一个测试报告。而只有报告和之前设计的测试用例同事过审,才能使领导相信,这个产品没有太大的问题,可以卖给客户了。
同样的,要让别人相信你写的程序(函数)没问题,最好的证明方式就是通过全面的测试数据和测试用例对你写的程序进行单元测试,然后得到一个测试报告,并且所有的这些都过审才能成为一个接受的单元测试结果,才能说服别人你的代码(函数)没有问题。
-
不写的借口:
- 编写单元测试太花时间了;
- 对于所编写的代码,你在调试上面花了多少时间。
- 对于以前你自认为正确的代码,而实际上这些代码却存在重大的bug,你花了多少时间在重新确认这些代码上面。
- 对于一个别人报告的bug,你花了多少时间才找出导致这个bug 的源码位置。
- 投入产出不成比例;
- 可以选择性的做单元测试
- 单元测试用例也可以有多有少啊
- 任务紧,没时间;
1)领导权衡的问题
-
-
定义
-
- 百度百科:
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
- 维基百科(中文版):
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
通常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书要求的工作目标,没有程序错误;虽然单元测试不是必须的,但也不坏,这牵涉到项目管理的政策决定。
每个理想的测试案例独立于其它案例;为测试时隔离模块,经常使用stubs、mock[1]或fake等测试马甲程序。单元测试通常由软件开发人员编写,用于确保他们所写的代码匹配软件需求和遵循开发目标。它的实施方式可以是非常手动的(透过纸笔),或者是做成构建自动化的一部分。
软件工程教材对单元测试的描述,编译也算单元测试的一部分。
我们编写代码时,一定会反复调试保证它能够编译通过。如果是编译没有通过的代码,没有任何人会愿意交付给自己的老板。但代码通过编译,只是说明了它的语法正确;我们却无法保证它的语义也一定正确,没有任何人可以轻易承诺这段代码的行为一定是正确的。
幸运的是,单元测试会为我们的承诺做保证。编写单元测试就是用来验证这段代码的行为是否与我们期望的一致。有了单元测试,我们可以自信的交付自己的代码,而没有任何的后顾之忧。
-
-
内容
-
白盒测试内容:
- 对程序模块的所有独立执行路径至少测试一次
- 对所有的逻辑判断,取“真”与取“假”的两种情况都能至少测试一次
- 在循环的边界和运行边界内执行循环体
- 测试内部数据结构的有效性
所以总结下来,单元测试的内容如下:
- 模块接口测试:即正常的输入、输出测试,如果数据不能正常输入和输出,那么还有测试的必要?
- 局部数据结构的测试:模块的局部数据是最常见的错误来源
- 路径测试:检查与偶遇计算错误、判定错误、控制流错误而导致的程序错误
- 错误处理测试:能在程序执行出错的情况下,进入既定的错误处理过程
- 边界测试:
-
方法
-
单元测试一般都以白盒测试的方法为主,辅以黑盒测试的方法。
- 规范导出法:根据相关的规范描述来设计测试用例,比如sip规范规定的某个字段的要求。
- 等价类划分:
- 边界值分析法、状态转移测试法、分支测试法、条件测试法、错误猜想法等
单元测试用例的设计就是要结合测试内容、测试方法。
- 单元测试用例设计的方法:
- 逻辑覆盖法
逻辑覆盖测试方法通常采用流程图来设计测试用例,它考察的重点是图中的判定框,因为这些判定通常是与选择结构有关或者循环结构有关,是决定程序结构的关键成分。
-
- 概述
- 覆盖标准
- 路径覆盖法
路径测试就是设计足够的测试用例,覆盖程序中每一条可能执行路径,至少测试一次,如果程序中含有循环(在程序图中表现为环)则每个循环至少执行一次。
-
单元测试模型
-
测试模型构建
-
构造单元测试模型的主要工作有:
- 构造最小运行调度系统,即设计驱动模块。
- 模拟实现单元接口,即设计桩模块。
- 模拟生成测试数据或者状态,为单元运行准备动态环境,即设计测试用例、测试结果。
测试模型除了能使被测对象运转起来之外,还应该考虑对测试过程的支持,例如对测试结果的保留,对测试覆盖率的记录等。
驱动模块和桩模块都是额外的开销,两种都属于必须开发,但又不能和最终软件一起提交的软件。驱动模块和桩模块为程序单元的执行构成了一个完整的环境,如图所示。
单元测试带来的好处:
单元测试的目标是隔离程序单元并验证其正确性。自动执行使目标达成更有效,也可获得本文上述单元测试收益。相反,不细心规划或者精心的单元测试可能被视为包括多个软件组件的集成测试案例,于是将因未完全达到创建单元测试的预定目标,测试可能失去较多收益。
在自动化测试时,为了实现隔离的效果,测试将脱离待测程序单元(或代码主体)本身固有的运行环境之外,即脱离产品环境或其本身被创建和调用的上下文环境,而在测试框架中运行。以隔离方式运行有利于充分显露待测试代码与其它程序单元或者产品数据空间的依赖关系。这些依赖关系在单元测试中可以被消除。
借助于自动化测试框架,开发人员可以抓住关键进行编码并通过测试去验证程序单元的正确性。在测试案例执行期间,框架通过日志记录了所有失败的测试准则。很多测试框架可以自动标记和提交失败的测试案例总结报告。根据失败的程度不同,框架可以中止后续测试。
总体说来,单元测试会激发程序员创造解耦的和内聚的代码体。单元测试实践有利于促进健康的软件开发习惯。设计模式、单元测试和重构经常一起出现在工作中,借助于它们,开发人员可以生产出最为完美的解决方案。
-
-
单元测试工具简介
-
- Cmockery,相对来说功能、使用难度、学习难度低的开源单元测试工具。可自写插件
- Ctest,是cmake内嵌的程序,用来提供测试cmake build出来的软件。不具备单元测试的框架、方法、环境的集成
- Check,c单元测试中的大牌,诸多开源项目都在用。与cmockery使用方式相似
- AceUnit,基于java的免费c单元测试框架,源码不免费。可自动成成一些东西,有一套严格的语法用来写单元测试。不开源
- TPT,专业付费测试软件。
总结:这些c单元测试框架统一得出的结论是,C的单元测试其实就是对于assert方法,free来检测内存泄漏的一个包装。
-
-
Cmockery使用介绍
-
基本来自cmockery官方指导书的翻译。
-
-
-
简介
-
-
Cmockery就是之前基本按照“单元测试模型构建”过程构建的一个单元测试模型框架。Cmockery与Cmockery库,标准C库,待测模块链接在一起,最终被编译成一个可以独立运行的程序。在测试过程中,待测模块的任何外部信息都应被模拟,即用测试用例中定义的函数返回值来替换。即使会出现待测代码在实际运行环境和测试环境运行时有差异,仍可视为有效。因为它的目的在于代码模块在功能面上的逻辑测试,不必要求所有的行为都和目标环境一致。
如果不做一些修改,无法将一个模块编译成可执行程序。因此 UNIT_TESTING 预定义应当定义在执行Cmockery单元测试的应用程序中, 编译待测代码时,可用条件编译决定是否参与单元测试。
-
-
-
使用
- run_tests()
-
-
Cmockery单元测试用例就是函数,签名为void function(void **state) . Cmockery 测试程序将(多个)测试用例的函数指针初始化到一个表中,使用unit_test*() 宏. 这个表会传给 run_tests() 宏来执行测试用例。 run_tests()将适当的异常/信号句柄,以及其他数据结构的指针装入到测试函数。当单元测试结束时, run_tests() 会显示出各种定义的测试是否成功。
这个就是传说中的测试驱动。
- 使用run_tests()
run_test.c
#include <stdarg.h> #include <stddef.h> #include <setjmp.h> #include <cmockery.h> // A test case that does nothing and succeeds. void null_test_success(void **state) { }文章来源:https://www.toymoban.com/news/detail-410791.html int main(int argc, char* argv[]) { const UnitTest tests[] = { unit_test(null_test_success), }; return run_tests(tests); } |
-
-
-
- assert_*()
-
-
Cmockery提供了一系列的断言宏,在测试程序的使用方法和C标准中的用法一致。 当断言错误发生时,Cmockery的断言宏会将这个这个错误输出到标准错误流,并把这个测试标记为失败。 由于标准C库中assert()的是限制,Cmockery的assert_true() 和 assert_false() 宏只能显示导致断言失败的表达式。Cmockery中和具体类型相关的断言宏, assert_{类型}_equal() and assert_{类型}_not_equal(), 显示那些导致断言失败的数据, 这样可以增加数据的可视化,辅助调试那些出错的测试用例。
- assert_{类型}_equal()宏的使用
例子 |
-
-
-
- mock(),will_return()
-
-
- 函数模拟
一个单元测试最好能将待测函数或模块从外部依赖中隔离。 这就会用到模拟函数,它通过动态或静态方式链接到待测模块中去。 当被测代码直接引用外部函数是,模拟函数必须静态链接。 动态链接是一个简单的过程,将一个函数指针放到一个表中,给待测模块中一个测试用例定义的模拟函数引用。
- (模拟函数)返回值
为了简化模拟函数的实现,Cmockery 提供了给模拟函数的每个测试用例存放返回值的功能,使用的是will_return() 函数。然后,这些值将通过每个模拟函数调用mock() 返回。 传给will_return() 的值,将分别添加到每个函数所特有的队列中去。连续调用 mock() ,将从函数的队列中移除一个返回值。 这使一个模拟函数通过多次调用mock() ,来返回(多个)输出参数和(一个)返回值成为可能。 此外,一个模拟函数多次调用(多个)返回值的做法也是可以的。
- will_return()的使用
例子 |
-
-
-
- check_expected(),expect_*()
-
-
- 参数检测
除了存储模拟函数的返回值之外,Cmockery还提供了对模拟函数参数期望值的存储功能,使用的是 expect_*()函数,一个模拟函数的参数可以通过check_expected()宏来做有效的验证.
连续调用expect_*()宏,是用队列中的一个参数值来检测给定的参数。 check_expected()检测一个的函数参数,它与expect_*()相对应,即将出队的值。 如果参数检验失败,这个测试将标记为失败。 此外,如果调用check_expected()时,队列中没有参数值出队,测试也会失败。
- expect_*()的使用
例子 |
-
VPBX实践
-
UT框架搭建
-
目录
-
-
|--bsu_prj |--H1 |--F1 |--sound |--*.wav |--NFV |--*.sh |--Linux |--Makefile |--buss.mak |--utest.mak |--*.sh |--*.sql |--version.h |--utest |--Makefile |--ut_main.c |--ut_common.h |--utest_*.c |--utest_*.h |
- 给每个网元的linux下增加一个utest目录,主要存放测试用例代码和桩代码
- 驱动模块代码:ut_main.c
- 目前没有对测试用例代码、桩代码进行分类,也没有对各个模块的的测试代码进行分类,所有的测试代码都放在utest目录下。需要的话后续可以加上按照模块分存
- 测试用例文件命名格式:.c文件,utest_+原文件名;.h同样。
- 测试用例命名格式:utest_+原函数名_+附加信息;如:utest_add_no_para_fail();utest_add_type_float();utest_add_type_uint();
-
-
编译:
- Makefile修改
-
-
- 编译采用以cmockery为主体,将vpbx编译到cmockery中编译方式。
- 正常编译时使用make,如果需要进行单元测试使用make ut type=1就可以编译出一个供单元测试的主程序,ut_bsu_test。
- Makefile中添加了UT_TEST宏,目的是编译出需要替换的桩函数。
-
-
- 代码修改
-
-
代码中主要的修改是:
- 将原来的main函数用UT_TEST宏进行控制,因为编出来的测试驱动主程序不能有两个main
- 第二种修改主要是根据进行UT测试时测试环境的搭建情况进行实时修改。目的是使用UT_TEST宏去控制到底是用桩函数还是使用原函数。
根据我们自己程序的实际情况,我们可能会违背一些之前文章中说的单元测试原则,隔离。因为我在编译cmockery框架时已经将vpbx整个连接进去了。通常情况下,我们可以构造一些环境,直接让被测程序调用到被测程序的内层程序(可能包括好几层),如果我们构造的单元测试环境够好的话,某些被测单元是不需要写桩函数的;我们也可以直接将被测单元内的函数使用桩函数替代,因为我们不是在测试被测单元内的函数,只需要他返回该返回的结果而已,毕竟我们的目的是在测试被测单元的流程、功能是否正确。当然,如果我们按照UT的基本思想来处理的话,当然第二种方式更合理,更符合UT的思想;实际使用中,那就不一定了。
比较:
使用桩函数,一,需要写桩函数;二,需要维护桩函数;三,需要在执行单元测试的时候用桩函数替换掉原函数(其实不好搞);
使用原函数,一,你得把原函数内所有的调用,这些调用的调用,调用的调用的调用…都得看一遍;二,看一遍的目的是为了构建测试环境,你要确保你调用的这个函数中的每个调用的测试环境你都构建了,否则你这个单元测试是走不下去的;三,其实从某种情况来说,这样的测试不能完全较单元测试了。
UT对应的应该是编码阶段,如果一个新的项目,通常来讲,都是各写各的代码,各写各的单元测试代码,各进行各自的单元测试。理论上不会像我们现在vpbx可以直接全部编译完成,然后可以调用被测单元内的原函数(这些函数可能由别人开发、测试),所以理论上使用桩函数是最好的。但是根据我们的实际情况,我们有条件去调用被测单元内的原函数,那就可以去构造测试环境然后调用。
以上分析可得到一个结论:针对同一个函数,如果有的单元测试用例要用桩函数,有的要用原函数,那么对于现在我的编译方法编出来的测试框架在执行不同的单元测试用例的时候就会有问题。所以要针对每个模块进行编译?会导致代码的不统一性。。。
-
-
实例demo
-
目前我已经做了cmockery能在我们的工程中进行单元测试的几个实例供大家参考。
-
-
-
例1:
-
-
类似黑盒测试,只需验证结果是否正常
int add(int a,int b) { return a+b; } void test_add(void **state) { assert_int_equal(add(3,3),6); assert_int_equal(add(-3,3),0); } void main() { const UnitTest tests[]= { unit_test(test_add), }; return run_tests(tests,"test_add") } |
-
-
-
例2:
-
-
入参检查,最简单的测试环境构建
UINT32 find_opcode_by_resource_name(char* pname,DMU_CFG_DATE_LIST* plist ) { //内容就是在plist 中找到 pname ,然后返回OPCODE //DMU_CFG_DATE_LIST 结构包含 name 和 opcode //找到return opcode;没找到return NULL; } //环境构造 DMU_CFG_DATE_LIST testlist[]={ {"test1",0x1111}, {"test2",0x1112}, {"test3",0x1113}, }; //入参检查单元测试 void test_find_opcode_by_resource_name_pname_null(void **state) { expect_assert_failues(find_opcode_by_resource_name(NULL,testlist)); } void test_find_opcode_by_resource_name_plist_null(void **state) { assert_int_equal(find_opcode_by_resource_name("aaa",NULL),0); } //pname 异常测试 void test_find_opcode_by_resource_name_pname_unusual(void **state) { pname="";//空字符串测试 pname1="abcdefghigk....";//超长字符串测试 assert_int_equal(find_opcode_by_resource_name(pname,testlist),0); expect_assert_failues(find_opcode_by_resource_name(pname1,testlist)); //assert_int_equal(find_opcode_by_resource_name(pname1,NULL),0); } void test_find_opcode_by_resource_name_list_has_no_pname(void **state) { assert_int_equal(find_opcode_by_resource_name("aaa",testlist),0); } void test_find_opcode_by_resource_name_list_nomal_find(void **state) { assert_int_equal(find_opcode_by_resource_name("test1",testlist),0x1111); assert_int_equal(find_opcode_by_resource_name("test2",testlist),0x1112); assert_int_equal(find_opcode_by_resource_name("test3",testlist),0x1113); } |
- 根据上下文得知,函数find_opcode_by_resource_name中的入参plist其实是一个全局变量,我们有两种方法去对find_opcode_by_resource_name进行测试。
- 一,自己构造plist;二,使用全局变量。在我来看自己构造应该更合理,我们的目的是对find_opcode_by_resource_name进行单元测试,对外部环境理论上应该是不在乎的。
- 这个其实最能说明一个问题:单元测试代码或者设计单元测试如果早于实际编码,可以对编码的质量有很好的提高作用。
- 写这个例子的过程就是这样,先定义函数的功能,确定出入参数,写个伪代码,然后开始根据出入参及返回值设计UT。
-
-
例3:
-
-
有返回值和出参的测试
/* /regest --->/regest ---->opcode /bsu/1/cfg --->/bsu/cfg /bsu/1/ten/11 --->/bsu/ten /bsu/2/ten/11/puis --->/bsu/ten/puis /bsu/2/ten/11/pui/1001/cfb --->/bsu/ten/pui/cfb */ //函数功能,将url 变成 可认识的字符串 UINT32 get_info_from_url(char *purl,DMU_URL_INFO* pinfo) { //出入参数有效性判断 //分析url,看url有几个/ ptemp=purl; while('\0'!=*ptemp) { if('/'==*ptemp) { num++; } ptemp++; } switch(num) { case 0: return 1; case 1: ... //把结果赋给pinfo带出 return 0; case 2: ... //把结果赋给pinfo带出 return 0; ... default: return 1; } return 0; } //utest 参数的有效性验证 //purl 异常验证,如空字符串,特别长的字符串 //正常purl /* DMU_URL_INFO { char aucShortUrl[DMU_STR_LEN]; char aucRes1[DMU_STR_LEN]; int ulUnitId; char aucRes2[DMU_STR_LEN]; int ulTenantId; char aucRes3[DMU_STR_LEN]; char aucRes4[DMU_STR_LEN]; } */ DMU_URL_INFO sttestlist={}; void utest_get_info_from_url_nomal(**state) { //首先判断函数返回值 assert_int_equal(get_info_from_url_nomal("/regest",&sttestlist),0); //如果在预期内再开始判断出参 assert_string_equal(sttestlist.aucShortUrl,"regest"); //首先判断函数返回值 assert_int_equal(get_info_from_url_nomal("/bsu/1/cfg",&sttestlist),0); //如果在预期内再开始判断出参 assert_string_equal(sttestlist.aucShortUrl,"/bsu/cfg"); assert_int_equal(sttestlist.ulUnitId,1); ... ... } void utest_get_info_from_url_unnomal(**state) { //首先判断函数返回值 assert_int_equal(get_info_from_url_nomal("/regest/ ",&sttestlist),0); //如果在预期内再开始判断出参 assert_string_equal(sttestlist.aucShortUrl,"regest"); assert_int_equal(get_info_from_url_nomal("regest/ //",&sttestlist),0); //如果在预期内再开始判断出参 assert_string_equal(sttestlist.aucShortUrl,"regest"); //首先判断函数返回值 assert_int_equal(get_info_from_url_nomal("/bsu/1//cfg",&sttestlist),0); //如果在预期内再开始判断出参 assert_string_equal(sttestlist.aucShortUrl,"/bsu/cfg"); assert_int_equal(sttestlist.ulUnitId,1); ... ... } |
这个例子想说明几个事情:
- 对于有出参的函数的UT的写法,先判断返回值,再判断出参的值
- 出参可能是个很大的结构体,我们可以根据自己测试需要对个别的进行判断
- 如果有switch那么理论上我们的UT用例和构造的数据要尽量做到对路径的全部覆盖
- 理论上switch可以使用类似{{0,fun()},…,…}这样的结构数组+查询函数find()替代,做UT时,只需要提前对find()进行单元测试就行,测试也只有两种情况,找到和找不到。但是在该例子中不合适,因为每个case会根据入参算出不同的出参,而我们对该函数的测试,有一个重点就是判断出参的值是否为预期值。
- 以前我对我写的get_info_from_url很有信心,但是当我写了几个单元测试的例子,就知道我的这个函数是漏洞百出。意思是,你在设计单元测试用例的时候就可能已经发现了很多被测函数的问题了。
- 当然,该例的测试用例(函数),是可以分开写的,比如有1个/的,2个/的;/连续的,/中间有空格的。。。等等
-
-
例4:
-
-
测试环境构造的两种方法
UINT32 cscf_regas_msg_set_string_content(CSCF_REGAS_STRING *pstInString, imp_sip_cs_IMP_STRING_STRUCT *pstOutString, IMP_SPM_MSG_CONTEXT *pstMsgContext, UINT32 ulbufflen) { //入参检查 //使用pstMsgContext给pout申请空间 //copy pin到pout return 0; } UINT32 cscf_regas_msg_set_part(CSCF_REGAS_SET_MSG_PART_T *pstIn, imp_sip_cs_PARTY_HEAD_T *pstOut, IMP_SPM_MSG_CONTEXT *pstMsgContext, UINT8 ucAddType, UINT8 ucPortTag, UINT8 ucUsernameTag, UINT8 ucOtherParaTag ) { //入参检查 //根据 type ,tag判断哪个字段需要申请编码空间 //pOut->pstSipUrl->psthost申请编码空间 cscf_regas_msg_set_string_content(in.ststring,out.sthost,pstMsgContext,len); //如果username tag =1 给pOut->pstSipUrl->pstUsername申请编码空间 cscf_regas_msg_set_string_content(in.ststring,out.stusername,pstMsgContext,len); ... ... } UINT32 cscf_regas_msg_set_common_header(CSCF_REGAS_INSTANCE_T *pstIn, imp_sip_cs_COMMON_HEADER_T *pstOut, IMP_SPM_MSG_CONTEXT *pstMsgContext, CSCF_REGAS_COMMON_TAG *pstTag; ) { //入参检查 //根据 pstTag判断哪个字段需要申请编码空间 //如果from tag =1 pOut->pstFromHeader申请编码空间 cscf_regas_msg_set_part(in.ststring,out.sthost,pstMsgContext,len); //如果to tag =1 给pOut->pstToHeader申请编码空间 cscf_regas_msg_set_part(in.ststring,out.stusername,pstMsgContext,len); ... ... } //入参为内部数据结构,全部都是数据结构,出参的结构为:每个tag后面跟一个指针,所以要给指针分配了 //空间才能使用 //其中pstMsgContext 是一个全局的结构数组空间的一个元素。一个4068空间的地址 void test_cscf_regas_msg_set_string_content_nomal(**state) { //环境构造 IMP_SPM_MSG_CONTEXT *pstMsgContext=NULL; imp_spm_msg_context_init(); pstMsgContext=imp_spm_msg_context_get(); //测试数据 CSCF_REGAS_STRING string ={18,"test.xxxx.com"}; imp_sip_cs_IMP_STRING_STRUCT *ststring=NULL; ststring=(imp_sip_cs_IMP_STRING_STRUCT*)imp_spm_msg_context_malloc(pstMsgContext,sizeof());
//断言 assert_int_equal(cscf_regas_msg_set_string_content(&string,ststring,pstMsgContext),0); assert_string_equal("test.xxxx.com",ststring->paucString); }
void test_cscf_regas_msg_set_string_content_nomal_other(**state) { //环境构造 IMP_SPM_MSG_CONTEXT stMsgContext={}; IMP_SPM_MSG_CONTEXT *pstMsgContext=&stMsgContext; pstMsgContext.pucMsg=pstMsgContext.aucMsgBuf
//测试数据 CSCF_REGAS_STRING string ={18,"test.xxxx.com"}; imp_sip_cs_IMP_STRING_STRUCT *ststring=NULL; ststring=(imp_sip_cs_IMP_STRING_STRUCT*)imp_spm_msg_context_malloc(pstMsgContext,sizeof()); //这个malloc会用到全局变量astSipContext,可能会导致问题 //断言 assert_int_equal(cscf_regas_msg_set_string_content(&string,ststring,pstMsgContext),0); assert_string_equal("test.xxxx.com",ststring->paucString); } |
这个例子想说明以下几个事情:
- 这种一步一步套着调用的函数,应该从最内层函数开始进行UT,然后一层一层的向外扩展做UT
- 当然也可以直接从外层开始写UT,但是内层的函数就需要打桩
- 像这种如果能构造环境将整个函数的调用层次都能包含的话,我建议还是直接构建一个合适的测试环境
- 比如,这个例子,经过分析后会发现,只要将pcontex_init的初始化,获取pcontex_get构造明白,整个函数使用都不会有问题。基本就不需要桩函数了。
- 经过分析pcontext其实就是个一片内存的首地址,如果pcontext_malloc时跟全局变量astpcontext无关的话完全可以使用test_cscf_regas_msg_set_string_content_nomal_other构造环境的方法对测试环境进行构造。
-
-
例5:
-
-
sql数据库测试环境构造
UINT32 rdms_regas_get_sipconfig(CSCF_REGAS_CONFIG_SIPCONFIG_TBL_T *pSipconfiginfo) { //入参检查 //数据库查询 ulret=vpbx_db_get_record_ex(g_db_handle,"SipConfig","",1,&num,pSipconfiginfo); //根据结果返回 return ulret; } //自建sqlit3环境 extern sqlite3* g_pMemDbConn; sqlite3* my_db_handle=NULL; UINT32 env_vpbx_db_init() { UINT32 ret=0; ret=sqlite3_open(":memory",&my_db_handle); ... } UINT32 env_vpbx_db_load_cfg(char * path) { //加载数据 }
void utest_rdms_regas_get_sipconfig_nmal(**state) { int ret; CSCF_REGAS_CONFIG_SIPCONFIG_TBL_T test_sipconfig={0};
bussunitsdb_function_init(); ret=vpbx_db_init(3);//3表示bsu if(ret!=0) { printf("error"); } vpbx_db_load_cfg("./utest/xxx.sql");
//ret=env_vpbx_db_init(); //ret=env_vpbx_db_load_cfg("./utest/xxx.sql");
assert_int_equal(rdms_regas_get_sipconfig(&test_sipconfig),0); //断言的依据是./utest/xxx.sql插入到sqlit的数据 assert_int_equal(test_sipconfig.port,6090); assert_int_equal(test_sipconfig.expires,3600); } |
该例子我想说明以下几件事情:
- 涉及到数据库,单元测试的时候必须得自己将数据库的环境整起来
- 方法,一,查看代码上下文,使用已有程序进行环境构建;二,自己写一个数据库的初始化过程;
- 其实过程都一样,创建sqlit句柄,使用句柄创建数据库,创建测试需要的库数据。然后使用方法连接数据库,进行数据库操作的测试。
- 理论上,我是想使用自己构建的环境测试的,但是发现vpbx_db_get_record_ex函数中有与句柄强相关的语句,比如这个句柄的全局变量。导致我不能很好的通过自己构建的环境去对rdms_regas_get_sipconfig做UT。必须使用(借用,因为还要改某些地方)我们之前已经写好的数据库初始过程。
- 通过例4,和例5可以发现,我们现在设计的程序,函数之间的耦合还是太强,导致不能完全自己构建测试环境对目标函数进行测试。这个情况其实可以很好的说明我们自己设计的程序还是不够合理。首先不是松耦合,当然这跟具体的情况有关,也许这样是最好的设计方式呢。
-
-
例6:
-
-
UT的最基本原则,隔离。
#define VPBX_CONF_FILE "./utest/xxx.sql" UINT32 oam_agent_load_cfg_file() { #ifndef UT_TEST ret=vpbx_db_load_cfg(VPBX_CONF_FILE); #else ret=mock_vpbx_db_load_cfg(VPBX_CONF_FILE); #endif if(ret!=0) { return 1; } //判断ret scms_sysctl_broadcast(MSG,NULL,0); return 0; } //mock unsigned int vpbx_db_load_cfg(char filename[]) { check_expected(filename); return (unsigned int)mock(); } //mock 1 unsigned int mock_vpbx_db_load_cfg(char filename[]) { check_expected(filename); return (unsigned int)mock(); } void utest_oam_agent_load_cfg_file_load_ok(void **state) { expect_string(vpbx_db_load_cfg,filename,VPBX_CONF_FILE); will_return(vpbx_db_load_cfg,0); //expect_string(mock_vpbx_db_load_cfg,filename,VPBX_CONF_FILE); //will_return(mock_vpbx_db_load_cfg,0); assert_int_equal(oam_agent_load_cfg_file(),0); } void utest_oam_agent_load_cfg_file_load_fail(void **state) { expect_string(vpbx_db_load_cfg,filename,VPBX_CONF_FILE); will_return(vpbx_db_load_cfg,1); //expect_string(mock_vpbx_db_load_cfg,filename,VPBX_CONF_FILE); //will_return(mock_vpbx_db_load_cfg,0); assert_int_equal(oam_agent_load_cfg_file(),1); } |
该例子我想说明以下几件事情:
- 函数的打桩模拟从理论上来讲才是UT的原则,因为我在做UT时只是针对oam_agent_load_cfg_file()的流程和结果进行测试,输出符合预期,全部路径覆盖到位基本可以认定这个函数是ok的了,对其中调用的函数只是需要他们的结果,过程可以不关心。
- 该针对int oam_agent_load_cfg_file()的UT,它自身包含两个调用vpbx_db_load_cfg和scms_sysctl_broadcast,其实对于被测函数本身,UT只是需要知道vpbx_db_load_cfg的结果就行,UT时只是根据结果走不同的分支而已,scms_sysctl_broadcast甚至可以不用关心,因为他没有返回值。
- 从例5可知vpbx_db_load_cfg其实是可以通过构建数据库环境直接调用运行的,但是对于这个被测函数,完全没有必要去搞这个过程,因为我的函数根本就没有用到创建了数据库环境后的结果。最主要的是我不关心这些。
- 所以就有了将vpbx_db_load_cfg调用在执行oam_agent_load_cfg_file()的UT测试时换成模拟函数的愿望,为啥呢?因为简单,隔离,不用受vpbx_db_load_cfg执行过程中出问题的影响。
- 假设调用vpbx_db_load_cfg时,他宕机了,走不下去了。你的oam_agent_load_cfg_file()的UT算不算对呢?
- vpbx_db_load_cfg的宕机,走不下去是由他自己的UT去保证的,我们不用去管。退一步将,如果我在写oam_agent_load_cfg_file()的UT和实现时,vpbx_db_load_cfg还没有实现那怎么办呢?
- 打桩是UT的最基本动作,它保证了UT的独立,隔离属性。理论上我认为所有的能打桩的尽量都去打桩,而不是调用已经完成的实现,因为你不能保证已完成的实现的质量。
那么怎么去将被测函数换成桩函数呢?
基本过程如下:
funA 调用 funB,再定义个mock_funB作为funB的模拟函数 | funA UT执行 真 statement1; | 实 statement2; | 执 funB -----换掉----- |-->mock_funB 行 statement3; | ... | V |
怎么换函数呢?
- 使用UT宏将函数隔开,替换,如之前的例子,需要增加代码量
- 使用UT宏将原函数隔开,新写同样名字的mock函数,编译后的到的就是mock后的函数
- Link的时候替换,要求要吧桩函数生成的.o和真正的.o分开弄。也许可以直接连接函数,这个不是很清楚
-
-
例7:
-
-
UT构造的模拟函数,带出参。
UINT32 cscf_regas_get_sbccfg(CSCF_REGAS_IMSENTRANCE_TBL_T *vpstImsinfo) { //入参检查 CSCF_REGAS_IMSENTRANCE_TBL_T stImsinfo={0}; #ifndef UT_TEST ulret=vpbx_db_get_record_ex(g_pMemDbconn,TB_NAME_sImsEntrance,"",1,&num,void*(stImsinfo)); #else ulret=mock_vpbx_db_get_record_ex(g_pMemDbconn,TB_NAME_sImsEntrance,"",1,&num,void*(stImsinfo)); #endif if(ulret==0||num==0) { //打印 return 1; } voss_mem_memcpy(vpstImsinfo,&stImsinfo,sizeof(CSCF_REGAS_IMSENTRANCE_TBL_T)); return 0; } /* CSCF_REGAS_IMSENTRANCE_TBL_T 结构体大致如下 { aucSbcinfo[32];ucDiscovrMode;aucPrimarySBC[33];aucSecondarySBC[33];....} */ int mock_vpbx_db_get_record_ex(sqlite *db,char tbl_name[],char strWhere[],int Max_count,int *num,void *poutBuf) { CSCF_REGAS_IMSENTRANCE_TBL_T test_buf={"xxxx.com",1,"sbc","sbc1",....}; memcpy(poutBuf,&test_buf,sizeof(CSCF_REGAS_IMSENTRANCE_TBL_T)); *num=1; check_expected(tbl_name); return (int)mock(); } void utest_cscf_regas_get_sbccfg_nomal_mock(**state) { CSCF_REGAS_IMSENTRANCE_TBL_T stImsinfo={0}; expect_string(mock_vpbx_db_get_record_ex,tbl_name,TB_NAME_sImsEntrance); will_return(mock_vpbx_db_get_record_ex,0); assert_int_equal(cscf_regas_get_sbccfg(&stImsinfo),0); assert_string_equal(stImsinfo.aucSbcinfo,"xxxx.com"); assert_int_equal(stImsinfo.ucDiscovrMode,1); assert_string_equal(stImsinfo.aucSecondarySBC,"sbc1"); } |
该例子的说明:文章来源地址https://www.toymoban.com/news/detail-410791.html
- 原函数cscf_regas_get_sbccfg(*pout)的目的是通过数据库查找sbc配置,用出参*pout带出。
- 原函数内需要调用数据库共有函数vpbx_db_get_record_ex该函数包括几个入参和两个出参,入参我们只关注tbl_name,出参由于才被测函数中都要用所以我们在模拟该函数时都需要想办法被带出。
- 我是这么设计的:
- 写模拟函数mock_vpbx_db_get_record_ex,第一由于我们关注tl_name,所以加一个check_expect(tl_name);其他的不关注所以不加入参校验
- 由于是模拟,我们可以直接定义一个固定的结构,然后复制给pout让他带出,同样的给*num的入参也直接赋值
- 这样做就可以模拟vpbx_db_get_record_ex了,因为其实我不关心vpbx_db_get_record_ex的实现,只要它的返回值和两个出参,我也不管它的出参是怎么来的,具体是查来的,还是赋死值来的。就我来所我在做模拟,所以直接赋死值比较快。。。直接赋死值就行。函数的目的达到了
- 先定义一个局部变量,死值,然后直接copy到pout就行
- 直接给*num赋1,让参数带出。
- 没毛病
- 然后在mock()
- 原函数需要使用#ifndef UT_TEST编译出来
- 其他过程同例6
-
遗留问题
- UT代码结构问题
- 使用mock后代码的编译问题
- 由2引起的UT的自动化问题
到了这里,关于单元测试方法-cmockery实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!