单元测试

对软件的一些基础元件及不同模块进行测试

单元测试

单元测试(Unit Testing),也称组件测试、模块测试,[0]是软件开发中的一种重要测试方法,[1][2]旨在对最小可测试单元进行检查和验证,[3]这些单元通常是函数、类或菜单等。单元测试的主要目的是隔离代码以确保它们按预期工作,[3]可以通过测试保障软件各项基本功能的完善性,并提升计算机运行的稳定性和安全性。[1][2]有效的单元测试具有可读性、可维护性、可依赖、快速执行的特性。[4]单元测试遵循制定测试计划、设计单元测试、测试实现和测试评估四步测试流程。[5][6]其测试策略可分为静态测试、单元结构测试、单元功能测试三种,[7][8]主要原则有FIRST原则、AIR原则和ASCII原则。[9]

单元测试(Unit Testing),也称组件测试、模块测试,[1]是软件开发中的一种重要测试方法,[2][3]旨在对最小可测试单元进行检查和验证,[4]这些单元通常是函数、类或菜单等。单元测试的主要目的是隔离代码以确保它们按预期工作,[4]可以通过测试保障软件各项基本功能的完善性,并提升计算机运行的稳定性和安全性。[2][3]

有效的单元测试具有可读性、可维护性、可依赖、快速执行的特性。[5]单元测试遵循制定测试计划、设计单元测试、测试实现和测试评估四步测试流程。[6][7]其测试策略可分为静态测试、单元结构测试、单元功能测试三种,[8][9]主要原则有FIRST原则、AIR原则和ASCII原则。[10]

单元测试通常由自动化单元测试工具执行,但也可以采用手动方法。[11]主要测试工具有JUnit[12]MATLAB[13]、pytest[14]等。单元测试可以更准、更全面地找到错误,提高整体效率和软件质量[6]但也存在花费时间成本高、容易受开发人员喜好影响、要求变化时负担大等缺点。[15]

基本概念

单元测试指的是检查和验证软件中的最小可测试单元。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。[4][1]

提高软件质量

由于单元测试所测试的对象是独立的单元,测试代码范围小,这就意味着测试更接近错误,只要简单地对每一个最小单元进行独立测试,全面准确地找到所有程序错误就很容易成为可能,而且更容易被定位和修复。[16]

削减开发时间和成本

问题被检测出来的时间越晚,花费的成本和时间会越多。单元测试可以较早地发现错误,减少时间和资源的浪费。此外,测试一个单元就可以发现其中的错误并改正,所以就不需要在以后花费大量时间重新了解和摸索。最后,受单元的相互作用和关联性影响,在单元级修改一个单元只会影响到原始的单元,这样就避免了各个单元之间的相互作用引发新的错误。[16]

测试原则

单元测试存在一些基本原则,有助于确保测试的有效性和可靠性。遵循这些原则可以提高软件质量,促进可维护性,以及增强代码的健壮性。关键的单元测试原则有FIRST原则、AIR原则、ASCII原则。[10]

字母含义具体解释
F-Fast(快速的)单元测试应该是可以快速运行的
I-Independent(独立的)单元测试应该是可以独立运行的,单元测试用例互相之间无依赖,且对外部资源也无任何依赖
R-Repeatable(可重复的)单元测试应该可以稳定重复的运行,并且每次运行的结果都是稳定可靠的
S-SelfValidating(自我验证的)单元测试应该是用例自动进行验证的,不能依赖人工验证
T-Timely(及时的)单元测试必须及时进行编写,更新和维护,以保证用例可以随着业务代码的变化动态的保障质量[10]
字母含义具体解释
A-Automatic(自动的)单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义
I-Independent(独立的)单元测试应该保持的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能对外部资源有所依赖
R-Repeatable(可重复的)单元测试是可以重复执行的,不能受到外界环境的影响。单元测试通常会被放入持续集成中,每次有代码提交时单元测试都会被执行[10]
字母含义具体解释
A-Automatic(自动的)单元测试应该是全自动执行的,并且非交互式的
S-SelfValidating(自我验证的)单元测试中必须使用断言方式来进行正确性验证,而不能根据输出进行人肉验证
C-Consistent(一致的)单元测试的参数和结果是确定且一致的
I-Independent(独立的)单元测试之间不能互相调用,也不能依赖执行的先后次序
I-Isolated(隔离的)单元测试需要是隔离的,不要依赖外部资源[10]

测试类型

单元测试通常由自动化单元测试工具执行,但也可以采用手动方法,这两种方法各有优劣。[11]

手动单元测试

手动单元测试依赖于能够理解复杂功能和特性的测试人员。 优势在于人类可以跳出框框思考,识别代码之外的问题并模拟用户体验。弊端是由于需要支付编码人员费用,所以手动单元测试成本较高,而且团队必须隔离各个组件并在每个组件上运行多个测试,也更加复杂。[11]

自动化单元测试

自动化单元测试使用程序和代码来执行测试,软件单元测试的工作速度更快,并限制了对其他组件的影响。 此外,开发人员可以多次重复使用。自动化单元测试的弊端是创建和维护必要的代码需要花费一定的时间,而且它不能测试出所有错误。[11]

测试策略

单元测试的策略可以归结为静态测试、单元结构测试、单元功能测试三个方面。静态测试不要求运行代码,在单元测试中静态测试主要指静态代码分析,而单元功能测试和结构测试则是动态测试。在动态测试中,被测试对象必须通过编译并且运行之,动态测试通过观察软件运行时的动作,来提供执行跟踪、时间分析以及测试覆盖度方面的信息。[8][9]

静态测试

单元测试的静态测试主要指静态代码分析,它不需要在计算机上执行程序,包括代码走读、代码审查和代码评审等方式。静态测试依据模块的详细设计,将代码与缺陷检查表进行对照,查看代码是否符合标准和规范。[8][9]

代码走读是非正式的一种交叉检查,就是自己的代码由他人来检查。代码审查是正式的、面对面的,以会议的形式展开,由大家根据缺陷检查表共同审核代码质量。代码评审通常在审查会进行,审查小组根据记录和报告进行评估。静态代码分析根据目的不同,可以分为三个层次,这三个层次可以按照从简单到复杂的顺序进行,具体要完成以下内容:

1.检查是否符合编程规范。

2.快速理解原代码,找出流程设计中的问题。

3.对原有代码的重构。

代码规范在实际操作中非常重要,以下面实例进行说明:[8]

package testSample; public class StyleSample{ int year,month,day; void setDate(int i,int j,int k) { year=i; month=j; day=k; } void printDate() { System.out.println('year='+year+',month='+month+',day='+day); } }

上面是一个Java代码,仅有13行,但采用较为严格的SUN规范检查的话,会存在41个不合规的地方,具体如下表:[8]

非法类型非法个数
行中含tab字符11
“X”后面不能有空格10
“X”前面不能有空格5
缺少注释4
参数X应该设置为final3
变量X必须是private,并且含有访问方法3
方法X没有设置成为可以拓展类型的,应该声明成abstract,final或者empty2
X应该在前面一行2
命名不规范1

单元结构测试

单元结构测试关注的是代码内部的执行情况,关注代码执行的覆盖率。在进行测试时,测试者必须检查程序的内部结构,从程序的逻辑结构着手,了解程序结构的实现细节等知识,得出测试数据。单元结构测试方法主要采用白盒测试,测试方法有基路径测试、循环测试、数据流的测试等。[8]

名称具体介绍
基路径测试基路径测试通过分析控制构造的环路复杂性,导出基本可执行路径的集合,从而设计测试用例,设计出的测试用例要保证在测试中程序的每个可执行语句至少执行一次[17]
循环测试循环测试专用于测试程序重点循环,注重于循环构造的有效性,并且可以进一步提高测试覆盖率,其目的在于检查循环结构的有效性[17]
数据流测试数据流测试是基于程序的控制流,从建立的数据目标状态的序列中发现异常[17]

在单元结构测试用例的设计过程中,其用例输入针对被测试函数的输入参数和全局变量两个部分。在测试用例输入数据的构造中,主要依据完整地覆盖单元本身的执行路径来设置,不同的执行路径需要设计不同的函数的输入和全局变量。具体步骤为识别测试单元,确定需要单元结构测试的函数或模块;分析内部结构,理解被测试单元的内部逻辑,包括执行路径、数据流、控制结构等;设计测试用例,包括确定用例输入和预期输出结果。[8]

单元功能测试

单元功能测试的基本方法是黑盒测试,它是已知产品所具有的功能,通过测试来检测每个功能是否都能正常使用。在测试时,把程序看作一个不能打开的黑盒子,在完全不考虑程序内部结构和内部特性的情况下,测试者对程序接口进行测试,它只检查程序功能是否能按照需求规格说明书的规定正常使用,常用的测试方法有边界值测试、等价类测试、因果图测试等。[8]

名称具体介绍
边界值测试边界值分析关注的是输入空间的边界,从中标识测试用例。边界值测试背后的基本原理是错误更可能出现在输入变量的极值附近,因此针对各种边界情况设计测试用例以发现更多错误[17]
等价类测试等价类测试是把所有可能的输入数据,即程序的输入域划分成若干部分(子集),然后从每一个子集中选取少数具有代表性的数据作为测试用例[17]
因果图测试因果图可以很清晰地描述各输入条件和输出结果的逻辑关系,最终生成判定表[17]

在测试设计过程中,其用例输入主要是针对被测试函数的参数和全局变量。测试用例输入数据的构造,主要以单元本身输入参数的范围来考虑,其次还要关注全局状态。具体设计中,可以将全局数据当成被测试单元的普通输入来考虑,接下来就可以采用不同的方法构造测试用例的输入了。同时,测试用例预期输出结果也包括被测试函数的返回值和全局变量的值。[8]

测试过程

单元测试流程和其他测试阶段一样,共分为四个步骤进行,分别是制定测试计划、设计单元测试、测试执行和测试评估。[6]

制定测试计划

在测试项目启动阶段,制定详细的测试计划和日程安排,包括单元测试的准备工作、测试策略的制定以及测试日程的计划。[6]在制定测试计划时,单元测试准备可以包括获取并理解项目的需求规格说明书、系统设计文档、项目计划等,为单元测试组织人员,并注意文档的版本和日期以确保准确性。制定单元测试策略是描述测试工程的总体方法和目标,包括采用的测试技术和工具,以及完成标准。单元测试日程计划则是根据工作量设定测试工程调度表,考虑特殊情况如模拟物理损坏、安全性威胁等。[6]

如以折半起泡排序为例进行单元测试,在制定测试计划阶段需要准备测试内容、项目计划、人员等,制定测试策略,并根据软件设计文档来评估测试内容和范围,然后根据工作量制定计划。[6]

设计单元测试

设计单元测试阶段的主要任务包括单元测试用例的编写,以及必要的驱动模块和桩模块代码的编写,依据为软件的详细设计说明书和单元测试计划书。[6]

以折半起泡排序为例进行单元测试,设计单元测试分两步进行。首先归纳出测试项,然后根据测试项设计测试用例。此阶段需要提交的文档包括测试设计说明书、测试用例说明书和测试程序代码。项目进度、测试粒度和测试密度这些因素影响测试设计的具体实施,包括定义单元的粒度和决定测试用例的覆盖率。[6]

测试执行

实施测试,可以包括在开发代码中加入断点,通过调用被测试函数并输入测试数据,或编写专门的测试代码进行测试。[6]

以折半起泡排序为例进行单元测试,运用自动化测试方法,执行结果出来后对其进行统计并生成测试执行报告和缺陷记录报告,对缺陷进行修复后进行回归测试[6]

测试评估

对测试结果进行评估和分析,提供测试数据和缺陷报告,评价被测对象并给出改进建议,最后整理测试文档并关闭测试项目。[6]

以折半起泡排序为例,在测试评估阶段由SQA组织会议评估,评估整理后提交单元测试总结报告。[6]

单元测试框架
单元测试框架

JUnit

JUnit是一个开源的Java测试框架,[12][18]JUnit的测试代码容易编写且功能强大,易于集成到开发的构建过程中,在软件构建过程中完成对程序的单元测试。[19]JUnit的测试方法可以自动执行,并且使用以assert为前缀的方法自动对比开发者的期望值和被测方法实际运行结果,然后返回给开发者一个测试成功或者失败的简明测试报告以提供及时的反馈。[19]JUnit已成为Java应用程序单元测试的事实标准,许多主流的Java开发平台都直接提供了对JUnit单元测试的支持,如Eclipse,Jbuilder等。[12][18]

JUnit 5
JUnit 5

MATLAB

MATLAB是编程和数值计算平台,支持数据分析、算法开发和建模。[20]MATLAB语言使用内置图形可视化数据和深入了解数据,可以对大型数据集运行分析,并扩展到集群和云。MATLAB代码可以与其他语言集成,帮助用户在Web、企业和生产系统中部署算法和应用程序。MATLAB单元测试框架支持三种测试编写方案:基于脚本的单元测试、基于函数的单元测试和基于类的单元测试。[13][21]MATLAB在人工智能、自动驾驶系统、FPGA、ASIC和SoC开发、企业和IT系统等领域得到广泛应用。[13]

基于脚本的单元测试

将每个单元测试编写为测试脚本文件的一个单独的节。开发人员可以执行基本验证,访问框架在测试结果上记录的诊断信息,通过选择要运行的测试来优化测试套件,以及通过创建和配置TestRunner对象来自定义测试运行。[21]

基于函数的单元测试

将每个单元测试编写为测试函数文件中的一个局部函数。基于函数的测试采用xUnit测试原理。除了支持基于脚本的测试所提供的功能之外,基于函数的测试还支持开发人员访问一组丰富的测试编写功能。[21]

基于类的单元测试

将每个单元测试编写为类定义文件中的一个Test方法。除了支持基于脚本和基于函数的测试所提供的功能之外,基于类的测试还为开发人员提供一些高级的测试编写功能,并支持开发人员访问完整的框架功能。[21]

MATLAB
MATLAB

pytest

pytest是Python的一种单元框架,具有入门简单,文档丰富;支持简单的单元测试和复杂的功能测试;支持参数化;支持重复执行失败的case;自定义扩展;方便地和持续集成工具集成等特点。[22]

利用py.test进行测试的实例如下:这里定义了一个被测试函数func,该函数将传递进来的参数加1后返回,还定义了一个测试函数test_func用来对func进行测试。test_func中使用基本的断言语句assert来对结果进行验证。[14]

#content of test_sample.py def func(x) return x+1 def test_func(): assert func(3)==5

$py.test ============test session starts============= platform linux--Python 3.4.1--py-1.4.27--pytest-2.7.1 rootdir:/tmp/doc-exec-101,inifile: collected 1 items test_sample.py F ============FAILURES============= _____________test_answer_______________ def test_answer(): >assert func(3)==5 E assert 4==5 E+where 4=func(3) test_sample.py:5:AssertionError ==============1 failed in 0.01 seconds=========

通过上面的测试输出,可以看到在该测试过程中收集到了一个测试函数,测试结果是失败的(标记为F),并且在FAILURES部分输出了详细的错误信息,以帮助分析失败原因。"assert func(3)==5"这条语句出错了,错误的原因是func(3)=4,然后可以断言func(3)等于5。[14]

pytest
pytest

对于软件的设计实现

高效发现错误:单元测试使开发人员能够提前识别出基于输入、输出或逻辑的错误,从而在不需要花费大量时间进行调试的情况下,快速定位错误所在的代码部分。[15]

充当文档:单元测试代码本身可以作为软件行为的文档,帮助开发人员理解代码在运行时的预期行为,并据此进行代码的修改或重构以优化性能和结构。[15]

促进重构:单元测试进行分支和覆盖分析,加强代码的可测试性,促进代码重构。[23]

提高整体效率:单元测试隔离软件的不同部分进行单个组件测试,避免问题组件影响其他组件的正常运行,提高整体效率。[11]

容易维护:单元测试促进了代码的可理解性,使得维护者更容易理解并维护代码。[23]

对于软件开发者

单元测试虽然给开发者增加了工作量,但对程序开发人员也有很多实际意义,如:帮助程序开发者更清晰地认识设计规格书中所要求的功能;有利于锻炼程序开发人员的逻辑思维能力、代码静态分析技能;促进代码编写标准的一致性;通过代码审阅能够让团队中的每个人参与到除了日常接触部分以外的其他领域;给团队提供学习机会。[23]

缺陷

单元测试的主要缺陷包括以下几点:

时间成本高

编写单元测试需要花费开发人员较多时间,而且在编写测试过程中可能因为重构而耗费更多精力,导致开发时间延长,预算不足的问题。[15]

受开发人员喜好影响

开发人员如果更加关注系统外部特征,对逻辑的要求降低,那么可能会减少对单元测试的使用。[15]

要求变化时负担大

在工作阶段内,不同项目对软件开发的要求随时可能变化,当要求变化时,开发代码块时所进行的单元测试会给开发团队造成一定负担。[15]

软件工程项目

随着软件系统规模的逐渐扩大,软件危机逐渐爆发,软件发展过程中存在的软件缺陷使人们更多地关注软件的安全性。软件测试贯穿软件开发的整个过程,在软件生产行业得到了迅速的发展和应用。单元测试一般是在编码后进行的第一步测试,在进行测试的过程中需要大量的测试输入,将测试输入的实际运行结果与期望的结果进行对比以验证程序是否正确。[24]一个良好的单元测试会在软件开发的某个阶段发现很多的bug,并且修改bug的成本低廉,能够有效避免在软件开发的后期阶段bug的发现和修改,节省时间和开发费用。[25]

极限编程

极限编程是一种致力于创建最高质量软件的软件开发思想,这种方法在很大程度上依赖于软件单元测试框架来进行全面的测试。 极限程序员经常使用自动化测试工具,以提高整体质量和响应能力,同时适应不断变化的客户需求。极限编程的指导原则之一是测试所有可能失败的东西,包括最小的组件。因此,单元测试对于极限程序员来说是一个强大的工具。[11]

汽车行业

单元测试在汽车行业同样得到大量应用,随着时代的发展,车辆的研发比以往更加依赖代码,即使出现轻微缺陷也可能造成危险情况。单元测试工具可以在汽车出厂前隔离代码以确定它的清晰度,并减少道路上出现故障的机会。[11]

敏捷开发

敏捷开发过程中,常用到单元测试方法。[26]敏捷开发(Agile Development)采用迭代和增量的方式进行开发,通常使用短开发周期(如2至4周)来完成一小部分功能,并经常与客户进行演示和反馈。[27]在最开始阶段,开发人员和需求人员要针对需求进行讨论,开发人员完成代码开发后做简单的测试,然后交付给测试人员做单元测试,完成单元测试后交付一版集成系统。测试人员将评审后的测试用例和前期的测试用例一起导入系统进行自动化集成测试,每次集成测试通过都要交付一版集成系统,记录代码的版本,并对实现需求列表进行记录。后续出现问题,可以对每一个内容备份到本地,进行版本验证,追溯是哪个版本出了问题,[26]这样可以快速验证假设、及时调整开发方向,并最大程度地满足客户需求。[27]

敏捷开发流程图
敏捷开发流程图

参考资料 27

  1. 参考 1
  2. 参考 2
  3. 参考 3
  4. 参考 4
  5. 如何写出有效的单元测试 — 51CTO
  6. 参考 6
  7. 参考 7
  8. 参考 8
  9. 参考 9
  10. 那些年,我们写过的无效单元测试 — 腾讯网
  11. 什么是单元测试? 深入了解流程、优势、挑战、工具等 — ZAPTEST
  12. 面向 Java 和 JVM 的程序员友好测试框架的第 5 个主要版本 — JUnit 5
  13. MathWorks
  14. 参考 14
  15. 什么是单元测试? — aws
  16. 参考 16
  17. 参考 17
  18. 参考 18
  19. 参考 19
  20. MATLAB数学·图形·编程 — MATLAB
  21. 编写单元测试的方法 — MathWorks
  22. pytest
  23. 参考 23
  24. 参考 24
  25. 参考 25
  26. 参考 26
  27. 参考 27
广告位:底部(ad-bottom)—— 请到中台「公共区块」编辑此内容