★北京广利核系统工程有限公司张海滨,高庆怡,黄太新,孔艳
关键词:核安全重要软件;单元测试;单元测试对象
软件单元测试在源代码级采用“X光”的方式测试代码,可以发现深层次的软件缺陷,是保障软件产品质量的重要验证活动。核电属于高安全领域,核安全重要仪控系统中的软件(以下简称核安全重要软件)不仅要实现软件需求文档的规定,而且要符合法规标准对软件的要求。核电相关标准都提出了对软件开展验证与确认(V&V)的要求,其中软件单元测试是软件V&V活动的重要组成部分,但对“单元”大小的定义及单元测试要求各标准描述不一致。
美国核管会(NRC)在导则RG1.168-2013认可的IEEE1012-2004中使用了术语“componenttest”,即component是“组成系统的部件,可以是硬件或软件也可以细分为其他组件”,并进一步说明了组件测试要确认软件组件正确实现了组件要求[1]。NRC通过导则RG1.171-2013认可的IEEE1008-1987(R2002)阐述了对单元测试的要求,其中明确了unit是一个包括一个或多个计算机程序模块及相应控制数据(例如表格)、使用规程、操作规程的模块集合[2],同时RG1.171-2013要求单元测试需满足安全功能要求覆盖和模块结构覆盖[3]。欧洲核标准IEC60880-2006要求模块验证应该表明每个模块执行其预期功能,不执行非预期功能[4],但未给出模块module的具体定义。国内能源行业标准“NB/T20054-2011核电厂安全重要仪表和控制系统执行A类功能的计算机软件”由IEC60880转化而来,两者要求相同。
“component”“unit”“module”各标准使用不同的“单元”术语定义,目前国际上尚未统一。各标准也提出了对软件单元测试的要求,本文认为RG1.171-2013高度概括了所有核标准对软件单元测试的要求,即满足安全功能要求覆盖和模块结构覆盖[3]。安全功能要求覆盖是指必须全面验证所有与核安全相关的功能需求,确保其在设计基准事故、预计运行事件及极端工况等情况下的正确执行,故在软件单元测试中需要针对安全功能完整性、边界条件、失效模式覆盖等开展测试。模块结构覆盖要求软件模块化设计满足功能内聚性、低耦合性、层次化架构的原则,在测试开展中需要针对模块接口、控制流覆盖、数据流覆盖进行测试。
因标准要求较为概括,通常国内核行业软件测试参考“GB/T15532-2008,计算机软件测试规范”中对单元测试的要求开展核安全重要软件单元测试活动。GB/T15532-2008规定软件单元测试的对象是可独立编译和汇编的程序模块(或称为软件构件或面向对象设计中的类)[5]。软件单元测试的目的是检查每个软件单元能否正确实现设计说明中的功能、性能、接口和其他设计约束等要求,发现单元内可能存在的各种差错[4],并且在其技术要求中提出了对软件结构覆盖(如分支覆盖100%)的要求。
1 核安全重要软件单元测试现状
1.1 单元测试常规方法
国内核行业综合考虑RG1.168-2013(认可IEEE1012-2004)、RG1.171-2013(认可IEEE1008-1987)、IEC60880-2006(等同NB/T20054-2011)的要求,参考中国软件行业推荐标准GB/T15532-2008,通常把标准中提到的不同术语的“单元”解读为“函数”(因函数是软件模块代码实现的最小粒度对象),并基于“函数”开展单元测试活动。
图1软件模块组成
一个完整的计算机程序通常由多个程序功能模块组成,每一个可独立编译的程序功能模块又通常由多个函数组成。某个功能模块示意图如图1所示,其中包括函数f1()~f11()。
按照国内核安全重要软件测试领域基于“函数”的单元测试方法,当需要对该程序功能模块开展单元测试时,通常会把该程序模块拆分成一个个独立的函数,根据详细设计文档设计测试用例、执行动态单元测试。例如对图1所示程序结构中要测试的指定函数f3(),创建驱动单元(driver)以调用函数f3(),对被f3()调用的函数f6()、f7(),用桩函数(stub)替换,如图2所示。
图2软件单元测试方法
核安全重要软件基本上都运行在现场控制设备中,是嵌入式软件;软件单元测试依据软件详细设计文档设计测试用例,在测试工具辅助下在主机上以仿真方式开展测试,其过程是通过分析被测试程序源代码,对被测试程序源代码进行插装,即插入收集控制流和数据流信息的探针代码。使用软件单元测试工具自动生成驱动函数和桩函数,根据执行测试用例的需要修改和补充测试驱动代码和桩代码,之后调试并仿真运行测试代码。在驱动函数和桩函数的配合下对被测试函数进行测试时探针代码被执行,探针代码通过某种方式输出测试结果和测试覆盖率信息。最后,通过界面把测试结果及代码覆盖情况呈现给测试人员,测试人员根据覆盖情况补充测试用例满足测试完备性和充分性要求,即达到要求的测试覆盖率。
使用该方法的优点是不受硬件的约束和限制,因为以单个独立的函数为对象,被测试函数里的几乎所有被调用函数都被打桩了,所以容易通过分析代码的处理逻辑,满足判定、条件为真为假的情况,从而达到高的结构测试覆盖率。1.2 单元测试常规方法的不足(1)测试工作量大常规方法对单个函数开展软件单元测试,需要花费大量的时间编写调试测试代码以完成单元测试,有时测试代码量会数倍于被测试代码,测试工作量大。(2)易导致片面追求高的结构覆盖率常规软件单元测试方法以达到具体的覆盖率为结束条件(比如100%MC/DC),以函数为对象较容易达到覆盖率要求,结果造成部分测试人员认为函数覆盖率达到了单元测试就完成了,软件单元测试就是覆盖率测试;其实,单个函数虽然达到了较高的覆盖率,但安全功能要求、逻辑实现仍可能存在缺陷。(3)难以全面覆盖真实用户场景桩函数是人为定义的,其函数返回值具有极强的目的性和针对性。常规软件单元测试时通过封装的桩函数返回预先定义的特定值来验证被测函数是否执行目标分支,这虽然能有效验证被测函数代码逻辑中路径分支的正确性,但人工模拟的方法难以全面覆盖真实用户应用场景下的各类复杂情况。
2 核安全重要软件单元测试新方法
2.1 基本原理
本单元测试方法以独立编译的程序功能模块为单元测试对象,对其不再拆分成单个的函数,而是使用单元测试工具对整个程序模块插装(instrument,即插入探针),然后对插入探针后的代码编译成为测试可执行程序,通过在真实环境中整体运行可执行程序,执行测试用例生成测试结果记录和覆盖率数据,然后通过查看代码覆盖情况补充测试用例完善测试,从实际用户使用的角度测试功能模块,在单元测试阶段关注安全功能要求的验证,暴露实际使用中可能存在的问题,可以在测试的早期阶段提高软件质量,减少缺陷遗留。
2.2 测试用例设计过程
遵循以上原理,为满足安全功能要求覆盖和结构覆盖,本方法测试用例设计过程与常规方法略有不同,分三步完成:
(1)首先,根据模块的安全功能要求设计测试用例。
(2)其次,根据设计文档中的设计细节补充基于设计的测试用例。
(3)最后,执行测试用例,根据测试覆盖情况,参考代码实现补充测试用例。
安全功能要求是代码实现的依据,根据模块安全功能要求设计的测试用例能够覆盖大部分代码,可以确保软件实现满足对模块的安全功能要求,避免设计和实现问题导致的副作用,并通过三步的测试用例设计与完善过程保证测试的充分性和完备性,这也是NRC认可的IEEE1008的附录B所建议的测试的增量式测试用例设计方法。
2.3 实施过程
本单元测试方法具体实施过程如图3所示。
图3 单元测试过程
本方法与常规方法的不同之处是,使用软件单元测试工具分析被测试程序源代码完成“代码插装”后,直接把包含探针信息的源代码编译“生成测试可执行程序”,以正常使用该功能模块的方式运行程序“执行测试用例”,即从该程序模块的外部接口进行测试数据的输入输出,以整体的方式开展测试活动。因为源代码是经过插装处理的,所以测试执行过程中探针代码就会把测试结果和覆盖信息输出。当测试用例执行完毕后使用单元测试工具分析测试结果和覆盖率信息,“查看覆盖率”判断覆盖率是否满足要求,如果发现代码覆盖率不满足要求,则分析代码没有覆盖的原因,然后补充相应的测试用例,直到达到要求的覆盖率。可见覆盖率只是测试充分性和完备性的指示器,并不是直接测试目标和目的。
2.4 新方法的优势
(1)真实验证模块安全功能要求
软件功能模块是作为有机整体对外交互的,不再是单个的孤立函数,是功能模块使用时的真实应用场景,测试的是功能模块的真实功能、性能等特性,从而能发现真实使用时可能出现的绝大多数缺陷。
(2)覆盖真实的调用接口
当以整个功能模块为单元测试对象,软件单元测试时就会覆盖代码原有的真实函数调用关系,减少接口问题的遗留。
(3)减少了测试工作量
因以整体的方式开展测试用例的设计和执行,执行顶层函数的一个用例就能覆盖其下层多个被调用函数,在保证测试覆盖的条件下,减少了测试用例的数量,而且以真实使用模块的方式进行输入输出,节省了大部分的测试代码编写、调试的工作,提高了测试工作效率。
(4)促进测试过程优化
当单元测试的粒度扩大后,可以探索把单元测试和集成测试,甚至产品确认测试(配置项测试)进行合并融合。尤其是对单一组件或组件较少的程序模块,可在一个测试阶段中完成多个阶段的动态测试工作,促进测试过程的优化。
该方法以比函数更大的功能模块为单元测试对象,在目标机上运行,能够完整覆盖所有的功能、性能等特性要求,满足安全功能要求覆盖和模块结构覆盖要求。当函数调用层次较多时,分析覆盖底层函数的难度提高,底层函数达到的最终结构覆盖率可能不及以单个函数为对象时高,但这是模块实际使用时真实能达到的结构覆盖率,更能反映实际运行中可能出现的情况,单元测试更有实际意义。
3 方法实验
以一个在QNX6.5操作系统下运行的示例程序模块为例进行说明,该程序完成根据输入的命令类型进行不同数据处理的功能,示例程序的调用结构如图4所示。
图4实验程序示意图
根据单元测试需要设计的试验环境如图5所示。
图5实验环境配置图
使用软件单元测试工具插入探针后,可通过某种方式输出测试结果和测试覆盖率信息,比如通过网络、串口等通信方式或者记录并输出到特定的文件中。本实验设置把测试执行结果信息和覆盖率信息输出到文件中。
建立上图所示的虚拟主机和虚拟目标机,主机使用Momentics4.7加上单元测试工具C++TestV9.6插件版,对C++Test的测试配置“Build test executable”脚本进行修改让其只生成插入探针后的测试可执行程序,通过测试配置生成测试可执行文件,然后复制测试可执行文件到QNX6.5的目标机上运行,给可执行程序不同的输入以测试不同情况并核对结果正确性。用例执行完毕停止程序运行后,把测试可执行程序生成的测试结果记录文件cpptest_results.tlog和覆盖率结果文件cpptest_results.clog放到C++Test工具指定的目录下,使用C++TestV9.6自带的“Load Test Results(Files)”工具菜单成功获取到了代码执行的覆盖率,可见该方法是有效可行的。
在真实环境中运行插入探针后的可执行程序,不断迭代查看代码覆盖情况补充测试用例完善测试这一过程,观察覆盖率输出值,发现覆盖率的增长逐渐趋于一稳定值但无法达到100%。经分析得出,由于不同软件的需求、设计、实现方式不同,可能存在部分软件功能无法从真实场景中制造的情况,使得软件运行始终无法触发某些代码,导致覆盖率稳定在某个非100%的值。这也表明该方法还存在一定局限性。
4 结论
本文以多函数组成的程序功能模块为软件单元测试的对象,基于模块安全功能要求和设计要求,参考代码实现设计测试用例,对代码插装编译成为测试可执行程序,在真实目标环境中验证功能模块满足核安全重要软件安全功能要求和结构覆盖要求,既降低了常规方法以函数为对象的软件单元测试的工作量,又以用户使用模块的角度真实验证了模块的安全功能和内部接口,从而提高了单元测试的价值,并提出了对单元测试和集成测试,甚至产品确认测试(配置项测试)进行融合的可能性,为优化软件测试过程提供了思路。
在测试工具C++Test的支持下,本文对单元测试新方法的可行性进行了实验,对无操作系统的软件的单元测试的适用性需要进一步研究;同时需要进一步研究使用专门的测试工具改进插装和读取覆盖率的便利性,以提高整个单元测试过程的自动化程度。
本文还提出了单元测试新方法存在的局限性,对于软件功能无法从真实场景制造的情况,会导致插入探针后代码覆盖率始终达不到100%。这也为后续研究提供了方向,研究人员将进一步研究解决特殊功能场景无法从真实环境制造导致代码覆盖率无法实现100%的问题,如开展采用新方法与传统方法相结合的可行性评估、结合指标的研究等,以进一步提高单元测试新方法的适用范围。
作者简介:
张海滨(1974-),男,河南开封人,工程师,硕士,现就职于北京广利核系统工程有限公司,主要从事核电仪控系统软件测试验证、软件质量评价工作。
参考文献:
[1] IEEE1012-2004, IEEE standard for software verification and validation[S].
[2] IEEE1008-1987(R2002), IEEE standard for software unit testing[S].
[3] RG1.171-2013, Software unit testing for digital computer software used in safety systems of nuclear power plants[S].
[4] IEC60880-2006, Nuclear power plants-Instrumentation and control systems important to safety-Software aspects for computerbased systems performing category A functions[S].
[5] GB/T15532-2008, 计算机软件测试规范[S].
摘自《自动化博览》2025年10月刊





案例频道