面向集控嵌入式实时软件的单元测试方法研究
2014-12-10裴承艳
裴承艳
(中国船舶重工集团公司第七一〇研究所,湖北 宜昌 443003)
0 引言
随着信息化军事技术的不断深入,嵌入式实时软件已在工业控制、电子信息以及武器装备等系统中发挥着越来越重要的作用;同时随着嵌入式软件的规模和复杂性的不断提高,作为有效保证和验证软件质量的重要环节和依据,软件测试已逐渐成为软件研制成本最高的阶段[1]。如何采用有效的嵌入式软件工程化测试方法提高嵌入式软件的质量和可靠性以及增强软件组织自身的软件测试能力具有极其重要的意义。
错误越早发现,项目付出的代价就越少,单元测试作为软件项目中最早介入的测试活动[2],易于发现程序的错误和缺陷,也易于实现代码测试的完全覆盖,因此单元测试的好坏对于软件质量的保证起着非常关键的作用。然而由于嵌入式软件的特殊性,如实时性强、与硬件紧密相关、访问硬件麻烦,在开发环境下模拟整个系统存在困难性,这使得测试一直是个难点,特别是单元测试,由于项目周期不允许,一些嵌入式软件没有进行单元测试或单元测试不彻底;有些嵌入式软件代码具有较高的耦合性,使单元测试难以进行;测试人员对于嵌入式软件单元测试过于依赖自动化测试工具,使测试效果不能令人满意,测试不规范,效率低[3],且无法确保嵌入式软件单元测试的充分性和有效性。
针对上述问题,本文以集控嵌入式软件为例,重点研究了基于Testbed软件测试工具的静态分析和动态测试方法,提出了一种较为完整和可操作的单元测试解决方法。研究分析了静态分析输出的度量模型值对嵌入式软件的影响,并根据度量值提出了提高软件代码质量的措施;介绍了基于Tornado编译环境的动态测试过程,并基于圈复杂度[4]提出了一种优先级的动态分析测试策略,以确保单元测试的充分性和有效性,提高软件测试方法的效率和规范性,确保软件的质量。
1 被测系统概述
被测系统集控软件是一个实时嵌入式系统,运行在集控模块控制单元内,控制单元由底板、CPU板和AD/DA板组成。板卡之间采用CPCI总线,通过CPU板上的两路LAN接口与通信集控柜连接,完成与操舵台、数采站、交流主配电柜、遥控操纵台、综控柜和显控台的网络信息交换。CPU板上的一路RS422A接口与通信集控柜中的导航定位分机连接,完成导航定位信息的读取;另一路RS422A接口与集控台内手控模块连接,完成指令的转发和手控状态的传输。控制单元中的AD/DA板与左/右主机齿轮箱执行器连接,完成速度调整。软件的总体结构图如图1所示。
图1 集控软件总体结构图
集控软件使用C++/C语言编写,程序基于模块化思想设计,在实时多任务操作系统VxWorks上实现,基于Tornado的开发环境。按功能划分模块,采用多任务下的同步机制,通过消息实现各个模块之间的通信。
在该软件的单元测试中,采用静态分析和动态测试相结合的方法来评估和完成软件的充分性和测试的完备程度。
2 集控软件测试关键步骤及实现
2.1 基于Tornado的仿真单元测试环境搭建
集控软件单元测试工具采用的是Testbed,它是英国LDRA公司开发的一种软件代码测试及分析工具,主要用在软件测试和软件维护阶段以便提高软件产品的质量,该工具可提供编码规则检查、软件度量分析、数据流分析、覆盖率分析等功能[5]。在 Testbed/Tbrun工具下配置集控嵌入式软件的仿真单元测试环境,需满足在Tornado2.2集成开发环境下成功编译、执行测试驱动程序,具体步骤如下:
(1)借助Tbconfig工具完成Tornado2.2编译环境的配置,并指定该开发环境的和测试工具的路径;
(2)配置生成被测函数的驱动模板C:LDRA_ToolsuiteVxworks路径下的vxworks_Cshlayout_663.dat,该模板用于生成被测函数的测试驱动;
(3)配置函数的插桩模板C:Testbed760VxworksVxworks_cinstr.dat,插桩模板的作用是对被测函数的入口、出口、控制流进行插桩,在单元测试结束时,用于分析单元测试的覆盖率,以确定测试用例是否满足覆盖率需求。
2.2 集控实时嵌入式软件的静态分析
静态分析是通过工具在非运行状态下对程序结构、数据结构、代码质量的分析,提取代码大量的静态内部信息,为代码审查以及动态测试提供辅助参考的信息[5]。下面以集控软件的网络通信模块UdpSocket.cpp为例,对其静态测试过程和结果进行详细说明。
2.2.1 静态分析过程
运行测试工具Testbed,打开UdpSocket.cpp源程序,选择MISRA编码规则,然后在Select Analysis窗口下选择分析菜单对该文件进行静态分析,通过该项分析,为测试人员提供了该文件中各函数之间的调用关系模型,图2帮助测试人员简单明了地以颜色区分来显示模块间的调用关系,红色为自定义函数,绿色为系统函数。图3是UdpSocket.cpp中各子函数通过度量的比例分析数,可得出总函数的度量为91%,清晰性为 93%,可维护性为91%,测试性为100%。图4是UdpSocket.cpp基于MaCabe的软件度量模型对程序分析的 Kiviat图,每一轴代表一类度量元,被测试源代码以扇形的结构显示出来,绿色表示符合质量标准。图5是UdpSocket.cpp基本节点数和基本圈复杂度数据分析柱状图。通过这些图可以帮助测试人员了解代码的静态内部信息,发现缺陷。
图2 UdpSocket.cpp调用关系
图3 UdpSocket.cpp度量比例
2.2.2 静态测试分析
图4 UdpSocket.cppKiviat图
图5 UdpSocket.cpp圈复杂度
静态分析的结果能够帮助测试人员从代码内部结构信息中开展工作,帮助质量管理人员从软件质量度量中进行质量监督[5]。通过对软件静态分析的总结,可以从降低代码的圈复杂度和提高代码的注释率两方面提高软件代码质量。
(1)降低代码的圈复杂度
圈复杂度是应用最广泛的静态度量之一,用来衡量一个函数判定结构的复杂程度,圈复杂度公式V(G)=P+1,P是代码中判定结点的数量[6]。程序的可能存在错误数和圈复杂度有着很大的相关性,圈复杂度越大代表程序代码的质量低并且难以维护和测试[6]。
当代码中遇到判定条件比较复杂时,可以将判定条件的表达式提前计算存储在一个变量中,简化判断条件,减低代码的圈复杂度,减少bug数。例如UdpSocket.cpp文件中有判定语句:
该判定条件的判定结点为3,圈复杂度即为4,将代码优化之后如下:
优化后的代码判定结点为1,圈复杂度为2。
(2)提高代码的注释率
提高代码的注释率可增加代码的可读性和可维护性,为每个代码块添加注释,并在每一层使用统一的注释方法和风格,包括每个类和每个方法。
2.3 集控实时嵌入式软件的动态测试
2.3.1 测试用例设计
(1)测试用例数据的合理设计
测试用例的设计是为了提高测试代码的覆盖率,动态测试中最重要的过程是如何设计测试用例,着重测试数据的输入设计。对于一般标准类型的输入变量(如int、char、float、double等)并没有多大问题,当变量是数组、指针、结构体、VxWorks中的 FUNCPTR、LOCAL等特殊类型时,数据输入就需要特别注意。当数组的下标值很大时,进行手工输入是不可能的,可以在TBrun环境中有选择地对需要的变量进行赋值或者在插桩后的代码中插入数组的初始化语句对整个数组进行赋值;当变量是指针时,由于不能给指针直接赋地址,输入指针采用映射的方法将指针变量映射成相应的自定义变量作为指针的输入值;结构体变量赋值不正确时很容易导致测试用例跑飞掉,需要从源代码找到该变量创建的赋值函数,将该函数作为变量的输入值,必要时还需要添加函数的参数,例如本文需测试的文件CUdpSocket.cpp中有消息队列数据结构体:
MSG_Q_ID msgId //接收消息的消息队列ID号其中MSG_Q_ID属于VxWorks的系统调用,在文件中有对msgId的赋值语句:
msgId=msgQGreate() //创建消息队列
在创建测试用例中对类型是MSG_Q_ID的变量输入值应设为 msgQGreate()。
(2)通过尽量少的测试用例达到尽量高的代码覆盖率。测试用例是以程序的内部结构为基础来设计的,需要尽可能多地覆盖程序的内部逻辑结构。
2.3.2 测试驱动的工作原理
动态测试过程中无法及时提供测试运行所需的真正目标机及其操作系统,必须正确配置开启仿真模拟器并将其作为虚拟目标机,将Testbed经过编译环境链接后生成的测试驱动程序下载到仿真模拟器中运行。每执行一个测试用例需要重新编译和执行,函数的驱动程序是由Testbed/Tbrun根据驱动模板自动生成的,驱动程序是一个基于控制台的程序,主要完成动态测试环境初始化,当调用测试用例时要执行函数ldra_qq_execute_test_case_1(),该函数负责被测函数入口参数的初始化,然后再调用被测函数完成整个测试的过程。
2.3.3 采用圈复杂度优先级的动态测试策略
有效的测试策略可使软件测试的效率最大化,从而满足测试的各项要求并降低测试成本。由于基于Tornado开发环境的集控软件代码规模较大,功能模块较多,结构复杂,因此为了提高测试效率,制定了一种基于优先级的动态测试策略,具体步骤如下:
(1)通过Testbed测试工具对每个文件静态分析的结果从基本节点数和基本圈复杂度数据来分析柱状图,得到每一个文件中被测函数的圈复杂度和节点数。
(2)按圈复杂度进行高低排序,对圈复杂度高且重要的函数进行重点测试。图5可看出UdpSocket.cpp文件中SocketSndData()函数的圈复杂度最高,因此首先对该函数进行重点测试。
(3)编译链接通过之后,执行设计好的测试用例,用监控到的控制流信息来分析程序的覆盖率,依据分析结果不断补充和优化测试用例。根据各模块的语句和分支覆盖率、已执行语句、执行路径以及未执行的语句,判定覆盖率并衡量是否完成动态测试活动。图6为Udp-Socket.cpp文件中SocketSndData()函数的动态测试图,图右上角可以得到该函数的语句分支覆盖率都为100%,图左下角是设计并执行通过的测试用例,右下角是用例数据的设计输入。
图 6 SocketSndData()动态测试图
3 结论
(1)图形界面框架类单元测试问题:由于 Testbed工具自动生成的测试驱动入口是 main()函数,类似于控制台程序,当遇到图形框架环境时无法完成测试。框架类(例如MFC,NI)应用程序的执行是以事件驱动面向对象的结构,定义了很多的API,应用程序可以直接调用。两种结构是完全不同的,当直接使用测试工具生成测试驱动时,由于缺少库文件,编译不能通过,无法执行测试用例进行测试。
(2)被测单元代码的必要修改:虽然大多数编译环境和Testbed工具关联在一起,但有一些代码单元还是不能直接执行测试,代码在动态测试之前必须做适当的修改,比如一些中断函数、死循环while()以及Forever等。
(3)测试人员不能过于依赖测试工具:自动化静态分析存在一定的机械性,测试人员需逐项进行分析确认出真正的问题所在,有效的测试不能简单的依靠测试工具。
[1]丁旭,崔吉岗,刘春裕.军用嵌入式软件结构覆盖测试技术[J].指挥控制与仿真,2008,30(3):120-122.
[2]李金麒,徐建平.嵌入式系统软件可靠性设计与测试方法[J].计算机系统应用,2013,22(1):74-78.
[3]肖波.通讯系统嵌入式平台下的单元测试技术研究[D].上海:华东师范大学,2005.
[4]孙梦磷,宋晓秋,巢翌.软件程序代码质量度量技术研究[J].计算机工程与设计,2006,27(2):325-327.
[5]张大林.基于缺陷关联的静态分析优化[J].软件学报,2014,25(2):386-399.
[6]阳凡林,康志忠.基于多维度覆盖率的软件测试动态评价方法[J].软件学报,2008,21(9):2135-2146.