我们怎样实现期望的TA?
我们从技术、流程、组织和个人四个维度,探讨怎样去实现我们所期望的,简单、可靠、可维护和可读的TA。
技术
-
充分使用Mock。在软件测试中,Mock无处不在。所谓Mock,就是把被测对象所依赖的外部对象用模拟器(Simulator)替代,并用这些模拟器来测试被测对象。无论被测对象是一个函数,还是一个复杂的设备,模拟器都很实用,有时甚至是不可或缺的。以一个包含两个软件(设备)A和B的系统S为例。对系统S的测试应当属于系统测试的范畴,而对A和B的测试则属于功能测试的范畴。由于在系统测试中发现的Bug,比在功能测试中发现的Bug的解决成本要高得多。因此,在进行系统测试前,我们务必要进行充分的功能测试,以保证能够在功能测试中发现的问题,尽量不要漏到系统测试中才被发现。那么问题来了,如果我们要测试A,但是A无法在没有B的情况下工作,怎么办?注意到,我们不能拿B来测试A,因为B是未经测试、不可信的(B的测试本身也依赖A)。这时,我们就需要一个B的模拟器。开发这样一个模拟器需要成本,但是这种投入是值得的,为什么?
-
模拟器易于实现。本质上,这个模拟器需要模拟的不是B的全部功能,而只是模拟A与B之间的接口。现代系统设计的不二法则是”高内聚,低耦合”。低耦合,意味着A与B之间的接口不会很复杂。B的模拟器只要实现这样一个接口就可以了。注意到,
模拟器的复杂度与被模拟对象真实的复杂度不存在正比关系。
通常,对于一个内部极其复杂的软件B,我们有可能只要开发一个轻量级的模拟器B就能够满足A的测试需求。反之亦然。由于模拟器的功能简单,因此通常易于实现、投入也低。
-
模拟器加快测试进度。模拟器一旦就绪,测试就可以开始。如果没有模拟器,测试被阻塞(Block)将是常态。在上面的例子中,A的每一个功能点完成后,都可以立即与模拟器B进行集成,而无须等待真实B对应功能的开发。只要模拟器B的开发严格遵从A-B间接口的规范,并由测试人员保证测试数据(样本)的正确性,那么与模拟器B的集成结果就足以验证A的功能。我们知道,测试工作开始得越早,软件问题发现得也就越早,修复软件问题的成本也就越低。所以,使用模拟器是一件既节省测试工作量、又改善测试效果的事情,可谓事半功倍。
-
模拟器助力软件问题的定位和解决。继续上面的例子。即使对A进行了充分地功能测试,我们仍然不能保证在后续的系统测试中,A不出现问题。如果在系统测试中,测试出A的功能问题,我们便需要对该问题进行修复。笔者认为:
软件测试的各个阶段(单元测试-组件测试-功能测试-系统测试),凡是在当前测试阶段所发现的软件Bug,一定能够在上一个测试阶段或更早的测试阶段中,通过修改或者增加一个测试用例来复现(Reproduce)。
之所以A的功能问题没有全部在功能测试中被发现,与模拟器的可信度、测试用例的覆盖度和测试数据的准确度有关系。很多情况下,这是难以彻底避免的。有模拟器的好处在于,一旦系统测试中发现问题,我们很容易在模拟环境中复现该问题。这有助于快速地验证软件改动的正确性。如果系统测试中发现A的功能问题,那么功能测试的TA Case也应当做对应的修改。并且,只要软件改动能够通过修改后的功能测试TA Case的验证,那么在真实环境中对应的问题通常就会得到解决。这意味着,我们无须通过系统测试去验证我们的改动,而系统验证通常是周期长、效率低、反馈慢的。使用模拟器,软件问题修复的效率得到了提高。
-
-
充分测试TA自身。没有经过测试的软件,其可用性(Availability)存疑。同样,如果TA软件自身没有经过充分地测试,如何能够被用来测试产品软件的质量?可以说,TA自身的测试是极其必要的。TA Library(包括模拟器)由于其在测试系统中承上启下的地位,严格的测试更是必不可少。只有充分的测试才能保证TA Library的质量,才能保证其可用性。
-
将每一个来自TA Case的需求以UT的形式体现,然后再进行开发 。TA Library功能简单、代码通常比较轻量,容易做单元测试(Unit Test, UT),适合实践TDD(测试驱动开发)。TA Case是TA Library的需求方。对来自TA Case的每一个需求,首先将其转换成一个或多个UT Case。然后,我们的代码实现以对应的UT Case通过为目标。只要UT Case通过了,一般来说我们能保证TA Case的需求得到满足。UT的另外一个好处是执行速度非常快。代码每做一点改动,改动的效果就会从UT Case执行的结果中看出来。例如,我们开发的一个实现模拟器功能的TA Library,总共有近200个UT Case,在普通Linux虚拟机上,10秒钟左右就能执行完毕,给出反馈结果。完整的UT覆盖加快了开发速度、提高了开发质量。在半年多时间内,我们Gitlab项目一共有1800次提交,完成了186个Task,合并了192个Merge Request,发布了216个版本。这些数据背后,均得益于我们从一开始就坚持UT驱动开发,并持续改进我们的UT。当然,UT也有另一个好处,即确保后期的代码改动不破坏已有功能。
-
在TA Library的开发中,使用Mock技术。通常,TA Library是先于被测软件存在的。在没有被测软件的情况下,如何测试TA Library的功能?同样,我们需要Mock。还是基于上面的例子。为了测试A,我们需要开发一个B的模拟器。那么为了测试模拟器B的功能,我们反过来也需要对A进行Mock。这个Mock的A存在于模拟器B的单元测试中。我们知道,模拟器B相对真实A,是足够简单的。同理,相对模拟器B,此时的Mock A也必须足够简单。只有足够简单的东西,才足够稳定、可信赖。事实上,我们可能只需要直接调用一些第三方库,并输入若干样本数据就可以开发出Mock的A,以实现测试模拟器B的需求。
-
对于每一个TA Case测出的问题,进行UT重现。每一个TA Case测出的TA Library问题,一定能够通过修改或增加一个TA Library的UT Case来复现。基于修改或增加后的UT Case,我们去修复TA Library的问题。代码修复完成的标准,是新的UT Case通过。而新的UT Case一旦通过,我们通常可以断定TA Case发现的问题不会再出现。因此,我们只需要本地验证后,即可以放心地发布升级的TA Library版本,部署给测试人员使用,而无需发布一个Debug版本让测试人员去验证我们的修复效果。 基于这种流程,UT会变得越来越完善,后续的UT设计也会参考前面的经验教训,并引以为鉴。
-
持续的重构和优化。对任何软件开发来说,重构和优化只有进行时,没有完成时。重构是手段,优化是目的。需要强调的是,不要等到所有代码完成之后,再进行重构。重构应当与代码开发同时进行。写代码的过程,也是重构和优化代码的过程。TA Library和TA Case都应该被持续地重构和优化。
流程
-
- TA 需求梳理。需求梳理(Grooming)是软件工程中的重要一环,有助于业务、开发、测试多方对软件的功能和预期结果形成一致的理解。由于TA本质上也是一种软件活动,因此TA的需求梳理是不可或缺的。这意味着,
一旦引入TA,软件测试的流程需要增加TA需求梳理这一环节。
TA需求梳理需要澄清: (1) 哪些测试用例需要自动化?(2) 为了实现用例的自动化,需要测试人员做些什么? 需要TA Library提供什么样的支持?(3) TA Library/TA Case的实现计划和安排 (4) 风险估计。通常来说,TA需求梳理应该尽可能早地进行,可以和产品软件的需求梳理同步或稍微滞后,以给测试人员和TA Library开发人员提供充足的时间去实现代码。
- TA 代码Review。和所有的软件代码一样,测试代码也要经过Review才能被接受。测试代码的Review包含TA Library的Review和TA Case的Review。前者和软件代码的Review类似,在此不赘述。后者则需要着重强调一下。在Review过程中,我们特别强调TA Case的可读性,希望阅读TA Case就像阅读需求文档一样轻松、简单、明了。具体来说,我们强调Case风格的一致性和Case描述的完整性。对于风格的一致性,我们要求任何一个测试人员写出的任何一个测试Case,从代码角度,必须符合同样的、前后一致的代码规范。这有利于避免因风格差异造成的阅读理解困难。对于描述的完整性,我们要求每一个测试步骤的描述应当完整,以”主语-谓语-宾语”的形式,把测试步骤所做的事情讲清楚。一些测试框架,例如Robot Framework,为我们用自然语言自由地表达TA Case提供了技术支撑。在具体操作上,对要进入TA回归测试集的Case,我们通过Room Meeting的形式来Review。具体来说,把TA Case投影到大屏幕上,相关人员围坐在一起,对TA Case进行逐字逐句的分析和点评。这样的Review,既能收获到许多的意见(Comments),也能够起到举一反三的效果。我们还有个小技巧。在Room Meeting Review时,不由作者本人,而随机由其他人讲解。作者本人只负责回应和记录Comments。这样能够很好的检验Case的可读性。因为如果Case的可读性足够好,那么任何稍有背景知识的人都应当能够在第一次阅读Case的时候就能基本理解Case的含义。
组织
-
软件设计。软件的顶层设计,影响软件可测性,也影响TA的效果。从测试角度来说,软件的可测性是测试工作开展的前提条件。就TA而言,其对软件可测性有更高的要求。TA要求所有与被测对象交互的外部对象都是可以Mock(模拟)的。并且,对象之间接口的行为和数据是可预测的(Predictable)。实际上,这个要求与软件设计”高内聚,低耦合”的要求是高度吻合的。如果一个软件的正常行为依赖于一个无法被Mock的外部模块,那么这个软件的功能测试将受制于对应模块的实现。这将阻塞软件测试的进行,TA也一起受阻。
-
软件开发。产品软件和TA Case,通常可以基于相同的需求/设计文档来实现。产品软件的开发是为了满足软件设计的目标,而TA Case的开发是为了检验产品软件的实现是否确实满足软件设计的目标。基于一致的目的, 软件的开发和TA Case的开发应该紧密地协同进行。需求或设计一旦发生变化,变化都应当及时地在软件开发和TA开发之间同步。
-
项目管理。对于项目管理人员来,如期地、高质量地交付软件是根本目标。TA将测试工作提速, 并持续不断地驱动测试和开发人员去修复高频率、大强度测试过程中发现的所有问题。这提升了软件质量,降低了软件交付后的风险。从项目管理角度,对TA的投入是值得的。项目管理人员应当支持,尤其应当支持早期软件测试阶段的自动化。
-
CI。伟大的软件公司一定有伟大的CI系统。CI输出的软件,应该以用户可用为目标。也就是说,CI须是面向交付的。一个面向交付的CI系统,要尽可能地将软件的各个测试和验证阶段逐步纳入CI的链条中来。软件工程的所有参与者都应该通过CI系统协同工作,实现共同的目标。对于任何一个测试阶段来说,进入CI的必要条件是测试能够实现自动化。TA是测试进CI的前提。另外,在实践中我们发现,由CI统一提供和维护标准化的、可靠的、私有的测试环境,比测试人员各自在本地搭建和维护测试环境要高效得多。当CI所承担的基础工作越来越多时,加大对CI的投入也是必要的。
个人
-
测试人员更懂开发。TA本质上是软件活动,是将测试工作以程序的形式实现并交给计算机去执行。我们发现,许多测试人员从未写过一行代码,但是在参与了一段时间的TA实践后,也能够完成数以百行的、可正常工作的测试代码,从而具备了一定的编程能力。另外,在编写、调试和运行TA Case的过程中,测试人员会遇到许多TA Case自身的问题,而这种问题本质上就是软件问题。解决这些问题的过程,也就是自身软件能力提升的过程。笔者认为,
TA的持续实践,无形中促进了测试工程师向测试开发工程师的蜕变。
测试人员本身就懂系统,通过TA的实践又具备了一定的编程技能,尤其是系统编程(System Programming)技能。这将大大地促进测试人员的职业发展,并且对部门和公司的发展也是有利的。但是,测试人员需要意识到,编程能力的提升绝不是一朝一夕的事情,需要自我成长的意识和长期的实践,久久为功。”在游泳在学会游泳”。同理,在编程中学会编程,在持续地代码重构和优化中学会编程,在不断地探索、接触和运用新技术中学会编程。
-
开发人员更懂测试 。与手动测试相比,TA使得测试人员和开发人员的工作关系更加紧密。对开发人员来说,与测试人员的协同工作,可以加深测试的理念(Mindset)。每一个软件开发人员都须明白,
软件的开发应该以验收性测试通过、用户可使用为目的,没有通过测试检验的代码是没有价值的。
有了牢固的测试理念,开发人员可能开发出更好的代码。通常来说,开发人员需要对软件功能的交付(Delivery)负责,而交付之前必要的环节就是软件测试。在TA的场景下,由于TA Case的目标与软件设计目标高度相关,并且CI提供测试环境、自动触发TA Case执行,开发人员完全可以自己去实现TA Case。在有TA Library的支撑下,开发人员实现TA Case没有技术难度。开发人员去实现TA Case和软件代码,并通过CI自动化地验证,将有效地节省开发与测试分割所带来的沟通成本和资源消耗。
结语
需要补充说明的是,不是所有手动测试都有必要进行自动化,也不是所有手动测试都容易实现自动化。做不做自动化?如何自动化?要结合实际情况,做出理性选择。本文的讨论限定于必要且适合做测试自动化的场景。无论如何,相比传统的手动测试,自动化测试确实是软件测试的一个重大变革。作为生产力的变革,测试自动化给测试自身带来了许多积极变化。另一方面,根据辩证法,生产关系要适应生产力的发展。软件测试生产力的提升必然对软件工程生产关系提出新的要求。因此,为了更好地发挥测试自动化的作用,无论是组织还是流程,都需要有一些积极的应变。我们要清醒地认识到,测试自动化的潜在效益绝非轻而易举可以获得,需要组织、流程的帮助,更需要测试人员自身的持续努力。
如果以历史演进的角度审视测试自动化,我们会发现一个有趣的现象。首先,测试自动化把测试人员从手动测试中解放了出来。然后,由持续集成系统自动地触发和执行测试,又再次解放了测试人员。倘若自动化测试用例由编程经验更丰富的开发人员去实现,是不是对测试人员的进一步解放?但是,可以确定,测试工作并非都可以被替代。解放后的测试人员,可以去设计更好、更完整的测试用例,去进行探索性测试,去向着更有挑战性的目标前进。
我是肖哥shelwin,一个高质量软件工程实践者和推动者。欢迎扫描下方二维码,添加我的个人公众号测试不将就,获得更多自动化测试, 持续集成, 软件工程实践, Python编程等领域原创文章。