【器篇395】云原生可测试性——谈软件的「可测试性」
软件可测试性对软件研发和质量保障有着至关重要的作用,是实现高质量、高效率交付的基础。可测试性差,会直接增加测试成本,让测试的结果验证变得困难,进而会让工程师不愿意做测试,或者让测试活动延迟发生,这些都违背了“持续测试,尽早以低成本发现问题”的原则。为此我们有必要对可测试性进行一次深入浅出的探讨,主要内容包含以下 五个方面。
01 可测试性的定义
软件的可测试性是指在一定的时间和成本前提下,进行测试设计、测试执行以此来发现软件的问题,以及发现故障并隔离、定位其故障的能力特性。简单的说,软件的可测试性就是一个计算机程序能够被测试的容易程度。一般来说可测试性很好的软件必然是一个强内聚、弱耦合、接口明确、意图明晰的软件,而不具可测试性的软件往往具有过强的耦合和混乱的逻辑。02 可测试性的四个特性
可测试性的分类方法很多不同的版本。比如由Microsoft提出的SOCK可测试性模型。总结起来,将其定义成可控制性、可观测性、可追踪性与可理解性四个维度。下面我们依次展开讨论。(1)可观察:能否容易的观察程序的行为、输入和输出。
任何一项操作或输入都应该有预期的、明确的响应或输出,不管是正确的还是错误的甚至是异常的,这样软件才是可测的;
当前、过去的系统状态和变量应可见或可查询,“可见”是指运行时可见、维护时可见或者调试时可见,“不可见”就“不可发现”,可测试性就低;
所有影响输出的因素可见;
可自动检测、报告内部错误;
错误输出易于识别,无论通过日志自动分析还是界面高亮显示的方式,要能有助于发现;
增加输出参数、减少变量重用,包括打印内部信息、将局部变量作为输出、增加断言、增加局部变量等。
最后一条尤其重要,看见的前提是输出,而且为了容易测试还要多多输出。这里需要给大家介绍一下DRR的概念。
DRR(Domain/Range Ratio),是定义域和值域的比率,也可以理解成输入个数和输出个数的比率。DRR用于度量信息的丢失程度。DRR越大,信息越容易丢失,错误越容易隐藏,测试性也就越低。(输入个数多于输出个数,多个输入只能得到相同的输出,意味着信息丢失)
因此要降低DRR,在输入个数不变的条件下,就要增加输出个数,输出参数越多,能获取的信息越多,也就越容易发现错误。
(2)可控制:能否容易的控制程序的行为、输入和输出。
提供适当的手段,在外界直接或间接的控制系统的状态及变量,例如对于某些全局类型的变量、特殊结构等,可以进行分类并封装到接口中;
采用模块化设计,各模块支持独立测试,对于每个相对独立的模块设计专用的测试驱动和测试桩,模块异常时不影响其他模块的测试(控制测试范围,就能更快地分解、定位问题);
业务流程和场景易于分解,可以针对单独业务流程进行测试;
提供对外接口,便于构造测试环境模拟外部系统;
提供适当的手段,可以打开或关闭调试输出或打印函数。
测试人员、维护人员能够控制程序的流程、场景、输入和输出,才能有针对性的执行各种用例或实验。通过接口开放流程控制、参数读写功能,也是支持实现自动化测试的基础。
(3)可追踪:能否容易的跟踪程序的操作、状态、性能、错误、GUI事件以及通信情况。
跟踪记录关键函数的持续时间、外部库调用;
跟踪记录关键流程的函数执行过程、输入输出参数;
严格遵从日志规范,正确设置日志级别,输出日志格式标准统一的,便于自动查询和分析;
定义日志类别,区分安全类、业务类、性能类日志,便于问题分析;
约定不同类别、不同级别日志的作用及意义;
日志文件至少保存 15 天,以便对以“周”为频次发生的异常分析。
追踪的目的是可见。日志是一种维护性可见的方式;运行时以及调试时可见,可以通过专门的追踪回显代码或者各种追踪工具软件实现,这儿就不详细说了。
(4)可理解:是否提供了足够的信息,易于获取、易于理解。
03 可测试性的三个核心观点
在正式讨论可测试性的技术性细节之前,很有必要先把可测试性的核心观点先和大家对齐。我认为可测试性有三个核心观点。(01)可测试性是设计出来的
毋容置疑,可测试性不是与生俱来的,而是被设计和实现出来的。可测试性必须被明确地设计,并且正式纳入需求管理的范畴。在研发团队内,测试架构师应该牵头推动可测试性的建设,并和软件架构师、开发工程师和测试工程师达成一致。测试工程师和测试架构师应该是可测试性需求的提出者,并且负责可测试性方案的评估和确认。在研发过程中,可测试性的评估要尽早开始,一般始于需求分析和设计阶段,并贯穿研发全流程,所以可测试性不再是测试工程师的责任,而是整个研发团队的职责。(02)提升可测试性可以节省研发成本
良好的可测试性意味测试的时间成本和技术成本都会降低,同时还能提升自动化测试的可靠性与稳定性,降低自动化测试的成本。今天在可测试性上的前期投资,会带来后续测试成本的大幅度降低。今天多花的一块钱可以为将来节省十块钱,这点再次证明了“很多时候选择比努力更重要”这一观点。(03)关注可测试性可以提升软件质量
可测试性好的软件必然拥有高内聚、低耦合、接口定义明确、行为意图清晰的设计。在准备写新代码时,要问自己一些问题:“我将如何测试我的代码?我将如何在尽量不考虑运行环境因素的前提下编写自动化测试用例来验证代码的正确性?”如果你无法回答好这些问题,那么请重新设计你的接口和代码。当你在开发软件时,时常问自己“我将如何验证软件的行为是否符合预期”,并且愿意为了达成这个目标对软件进行良好的设计,作为回报,你将得到一个具有良好结构的系统。最后,我想说的是“质量是奢侈品,可测试性更是奢侈品中的奢侈品”。要让研发团队重视可测试性是件很难的事情。究其根本原因是因为研发团队“不够痛”。长久以来,测试和开发一直是分开的两个团队,开发工程师往往更关注功能的实现,充其量会去关注一些类似性能、安全和兼容性相关的非功能需求,对于可测试性基本是没有任何优先级的,因为测试工作并不是由开发工程师自己完成的,可测试性的价值开发工程师根本就感受不到。而测试工程师虽然饱受可测试性的各种折磨,可是又苦于在软件研发生命周期的下游,对此也无能为力,因为很多可测试性需求是需要在设计阶段就考虑并实现的,到了最后的测试阶段很多事情已经为时已晚。很多时候,你不想改是因为你不痛,你不愿改是因为你不够痛,只有真正痛过才知道改的价值。所以应该让让开发工程师自己承担测试工作,这样开发工程师会切身的感受到可测试性的重要性与价值,进而在设计与实现阶段赋予系统更优秀的可测试性,由此而来的良性循环能让系统整体可测性始终处于较高水平。这其实也是开发者自测能够带了的一个好处。关于开发者自测的话题,可以关注我之前在InfoQ上写的一篇热文“开发要不要自己做测试?怎么做?”。04 不同级别的可测试性表现
不同级别有不同的可测试性要求。下面我们分别从代码级别、服务级别和业务需求级别来分别展开讨论。01代码级别的可测试性
代码级别的可测试性是指针对代码编写单元测试的难易程度。对于一段被测代码,如果为其编写单元测试的难度很大,需要依赖很多“奇技淫巧”或者单元测试框架和Mock框架的高级特性,那往往就意味着代码实现得不够合理,代码的可测试性不好。如果你是资深的开发工程师,并且一直有写单元测试的习惯,你会发现写单元测试本身其实并不难,反倒是写出可测试性好的代码确是一件非常有挑战的事情。代码违反可测试性的反模式有很多,常见的有以下这些:02服务级别的可测试性
服务级别的可测试性主要是针对微服务来讲的。相对代码级别的可测试性,服务级别的可测试性更容易理解。一般来讲,服务级别的可测试性主要考虑以下这些方面:03业务需求级别的可测试性
业务级别的可测试性是最容易理解,也是平时大家接触最多的。一般来讲,业务级别的可测试性可以进一步细分为手工测试的可测试性和自动化测试的可测试性。业务需求级别的可测试性有以下典型的场景:05 可测试性设计与相关工程实践
软件的可测试性特征主要表现是设立观察点、控制点、观察装置、驱动装置、隔离装置。需要注意的是可测试性设计时必须要保证不能对软件系统的任何功能有影响,不能产生附加的活动或者附加的测试,采取合适的设计模式对软件进行设计。优先编写测试代码,这是标准的XP方法。不是说应该一次性编写全部测试代码后,再一次性全部实现。先写验收测试,再写单元测试编写一些测试代码,实现它们,再编写一些测试代码,再实现它们等等是个更好的办法。设计以这种方式得以进展;在实现阶段捕捉错误并在下一组测试中改正它,以这种方式编写测试也更少会使人畏缩。使用小型函数说明和重载带缺省参数的函数将使在测试中调用这些函数变的愉快的多。否则,在测试这些函数时将不得不构造额外参数,如果参数很大,那么将很快导致代码膨胀。更糟的是,它会诱使你编写比在其他情况下更少的测试。把代码移到 GUI 视图的外面。然后各种 GUI 动作就能成了模型上的简单方法调用。这样,对GUI测试者来说,通过方法调用测试功能比间接地测试功能容易的多。另一个好处是它使修改程序功能而不影响视图变的更容易。I.在外界使用适当的手段能够直接或间接控制该变量,包括获取、修改变量值等;II. 可以将全局类型的变量进行分类并封装到一个个接口中操作。各接口在外界使用适当的手段能够直接调用对该接口进行操作,这里所谓的适当的手段主要包括使用测试工具和增加额外代码. 对于向外提供的接口的接洽处能够人为的对接,比如构造测试环境模拟接口对接,这里所指的开放接口主要是指相对于整个被测系统,即为被测系统以外提供的接口。接口接洽处人为对接时各接口所要求的条件和所需的参数人为的能够轻易达到和提供。对于每个相对独立的模块设计好所需要的驱动和桩都能单独设计用例进行测试对应的功能,在测试运行期间模块异常时能够将其隔离而不影响测试。在测试环境满足的情况下能够控制任一单独业务流程,各业务流程具有流通性。将一场景所涉及到的业务和接口整合到一个统一的接口使其能够单独操作该场景。对于复杂的业务流程需合理设定分解点,在测试时能够对其进行分解。对于复杂的场景需合理设定分解点,在测试时能够对其进行分解。测试模块发布合理,不能在后期追加的模块为前期所测模块引入新的不必要的测试活动。为单个测试接口、测试业务、测试场景预留测试驱动和桩的接入点。在增量式开发过程中必须优先考虑测试桩和测试驱动实现的难易程度和真实性。1.注释需要详尽。特别对于接口,要描述清楚功能、实现及参数;3.为集成测试与系统联调准备调测开关及相应打印函数,并且要有详细的说明;4.为单元测试选择恰当的测试点,并仔细构造测试代码、测试用例,同时给出明确的注释说明。测试代码部分应作为(模块中的)一个子模块,以方便测试代码在模块中的安装与拆卸(通过调测开关);6.用断言来检查程序正常运行时不应发生但在调测时有可能发生的非法情况;7.为测试自动化工具提供所需要的特定“钩子(hook)”;8.对于每个功能,提供访问、修改“状态”变量的接口,包括提供查询、修改上层软件、软硬件接口、底层硬件状态的接口及打印;9.提供查询系统状态的接口。比如内存使用、程序使用进程数等;10.对于测试因为环境等因素而可能无法测试的功能,提供接口模拟软件实现该功能的过程;11.对于修改功能,提供修改功能参数单位的接口,以便于进行如软件性能等的测试;12.出错及异常处理保存记录,记录具有详细的属性,并且格式统一、意义明确;13.在程序异常时,除了保留日志,还需要提供观察、恢复的外部方法;1.对于程序中所涉及到的变量尽可能的在调试过程中可以查询及修改;2.在整个软件系统执行过程中为每个关键业务或相对独立的业务设定一个调试点,便于系统集成和问题范围的定位;3.在设定好的调试点处对处理的业务输出数据和全局数据进行可视化输出,便于测试结果的分析。传统文化复兴与赛博空间建设