印冉 研发效能(DevOps)技术工程师(中级)学员
摘要
传统的软件开发流程中,测试通常是在编码完成后进行的,这种做法可能会导致许多问题在后期才被发现,从而增加修复成本和风险。随着软件开发行业的成熟,软件测试方法也日趋成熟,开发团队正在逐渐自动化大部分的测试,以此取代大量测试人员手工测试。通过编写自动化测试,让测试前移,开发团队可以在提测前就知道他们的软件是否被破坏,而不是在测试后期才收到反馈。
关键词:测试左移,测试策略,测试稳定,测试质量
一、背景
传统的软件开发流程中,测试通常是在编码完成后进行的,这种做法可能会导致许多问题在后期才被发现,从而增加修复成本和风险。随着软件开发行业的成熟,软件测试方法也日趋成熟,开发团队正在逐渐自动化大部分的测试,以此取代大量测试人员手工测试。通过编写自动化测试,让测试前移,开发团队可以在提测前就知道他们的软件是否被破坏,而不是在测试后期才收到反馈。
但是,当大量的自动化测试存在于软件中时,可能会带来维护成本高(自动化测试代码也是软件的一部分,需要定期维护和更新,以适应软件功能的变更)、测试质量低(如果自动化测试本身编写不规范或者质量不高,可能会导致错误的测试结果,进而误导开发团队)、资源消耗提升等风险(大量的自动化测试会增加CI/CD(持续集成/持续部署)流程的运行时间,消耗更多的计算资源)。
为了解决上述问题,选择合适的测试策略、保障测试的稳定性及编写高质量的测试,便是我们减少维护成本、保障测试质量和提升反馈效率的主要手段。
二、选择合适的测试策略
(一)按测试测试金字塔进行分类
测试金字塔,是一种方法论,是自动化测试分层覆盖情况的一个参考模型。
(图1 测试金字塔模型图)
测试金字塔告诉我们测试是分层的,从下往上测试的运行速度是逐渐减慢的,外物依赖或者服务间的依赖从下到上会依赖更多,从下往上对每一层的测试代码是逐层减少的。下方应该写一些小而快的测试,往上应该编写一些粗粒度的测试,编写更少的高层次测试。
(二)按测试尺寸大小分类
根据Google对测试的定义,我们可以将测试按测试尺寸大小分类。测试的大小取决于其运行方式,允许执行的操作以及消耗的资源数量。
(图2 Google对测试尺寸的分类图)
1.小型测试
-
必须在单个进程中运行,测试的代码必须在与被测试的代码相同的过程中运行
-
不允许它们进入睡眠状态,执行I/O操作,不允许进行小型测试来访问网络或磁盘
-
测试依赖访问或磁盘时需要使用测试替身
-
单元测试
2.中型测试
-
跨越多个进程,使用线程,并且可以对进行阻塞调用,包括网络调用
-
ocalhost;
-
不允许使用中级测试对除localhost以外的任何系统进行网络调用。换句话说,测试必须包含在一台机器中
-
集成测试,组件测试
3.大型测试
-
任意位置运行
-
要测试的系统可以跨越多台计算机
-
试用于全系统的端到端测试
-
端到端测试
(三)测试策略的演进
随着测试过程持续的推进,业务越来越复杂,测试用例越来越多,测试执行时间也越来越长。就需要在测试不断增长的情况下,调整测试策略,分时段、分环境执行测试,并逐渐调整测试的分布,实现测试价值的最大化。
-
当系统的复杂度较低,模块较少时,单元测试与集成测试覆盖率较高,可以提供足够的信心,单元测试可以占80%,集成测试10%,契约测试与端到端测试共占10%。
-
当系统的复杂度较高,模块较多时,需要增加高层测试的占比,以保证整体服务的正常协调。单元测试占60%,集成测试与组件测试占20%,端到端占15,契约测试5%。当调用的外部API较多时,应该增加契约测试的占比。
(图3 测试策略演进图)
三、保障测试的稳定性
(一)采用本地测试
当开发的软件系统依赖多个外部进程组件(服务/应用)进行协作才能工作时,为了对系统进行测试通常意味着你需要安装和配置一系列的外部依赖组件才能开始,而这通常需要花费更多时间和遭受更多噪音的影响,特别是依赖的外部服务接口不受控制的情况下。在测试驱动开发成为主流的高效软件开发实践的背景下,如何降低测试环境的依赖配置成本已经成为许多团队在选择技术框架、组件和外部应用服务的的主要考量。
作为测试依赖外部配置成本高低的一项显示信号就是在断开外部网络连接信号的情况,你的团队成员是否可以方便快速的在本地开发环境(通常是开发者的桌面电脑)完成所有的外部依赖配置,从支持单元测试、组件测试到端到端的验收测试。
本地:指的是开发人员的本地开发环境,也就是六步提交法中在的“个人工作区”。
本地测试:指的是在开发人员本地开发环境、不依赖外部环境就能够运行的测试用例。
(图4 本地测试原理图)
如何开展本地测试:
1.进程内方式开展本地测试
所有对外部的依赖都可以在内存中构造,使用类似于依赖注入的方式替换默认的实现。
-
外部服务(组件):内存中的存根(stub)
-
数据库、Redis等存储设备:内存实现
(图5 进程内本地测试原理图)
2.跨进程方式开展本地测试
尽量减少跨进程的访问,坚决杜绝跨网络(除localhost外)的访问。
-
外部服务(组件):使用测试替身(例如:mock-server)
-
数据库、Redis等存储设备:进程内实现
(图6 跨进程本地测试原理图)
(二)使用测试替身(Test Double)
"Test Double",Martin Fowler的文章测试替身(Test Double)中提出的概念,是一个通用的词,代表为了达到测试目的并且减少被测试对象的依赖,使用"替身"代替一个真实的依赖对象,保证了测试的速度和稳定性。
在项目中,我们时常会遇到由于待测系统依赖组件无法工作而造成的测试阻碍,这是严重影响项目交付的风险之一,而Test Double就是规避这个风险的手段。
在测试过程中,我们使用Test Double替代真实的依赖组件去和待测系统进行交互,Test Double不必和真实的依赖组件的实现一模一样,比如不用去实现依赖组件复杂的内部逻辑等,我们只需要在满足测试需求范围内,确保对于待测系统来说Test Double提供的API是和依赖组件提供的一样的,API是怎么实现的在这个上下文就显得不重要了。基于这个特点,Test Double多用于自动化测试比如单元测试和集成测试。
Test Double的类型:
-
Dummy Object(假设):指为了调用被测试方法而传入的假参数,为什么说是假参数呢?实际上这些传入的Dummy对象并不会对测试有任何作用,仅仅是为了成功调用被测试方法。
-
Test Stub(桩):指一个完全代替待测系统依赖组件的对象,这个对象按照我们设计的输出与待测系统进行交互,可以理解是在待测系统内部打的一个桩。这个桩既不会与测试用例(代码)交互,也不会在待测系统内部进行验证。Test Stub常用于响应待测系统的请求,然后返回特定的值。接下来,这个值会对待测系统产生影响,然后我们就在测试用例里面去验证这个影响。
-
Test Spy(间谍):指一个待测系统依赖组件的替身(被依赖对象的代理,行为往往由被代理的真实对象提供),并且会捕捉和保存待测对象对依赖系统的输出,这个输出会用于测试代码中的验证。Test Spy主要用于记录和验证待测对象对依赖系统的输出。
-
Mock Object(模拟):指一个完全代替待测系统依赖组件,并且用于验证待测系统输出的对象。这个对象接受待测系统的输出,进行处理并且这个输出进行验证,一旦验证通过也会返回值给待测系统。Mock Object主要用于接收待测系统的输出,然后进行验证。
-
Fake Object(伪装):指一个轻量级的完全代替待测系统依赖组件的对象,采用更加简单的方法实现依赖组件的功能。Fake Object可以是一个“fake DB”比如简单的内存数据库来代替真实的重量级的数据库,也可以是一个“fake web service”比如创建一个简单的web service来返回指定的response。
(图7 测试替身分类图)
下图为注册服务使用测试替身的示例图:
(图8 注册服务测试替身示例图)
四、编写高质量的测试
(一)了解常用的测试用例设计方法
-
等价类划分法:是一种典型的黑盒测试方法,用这一方法设计测试用例完全不考虑程序的内部结构。把程序的输入域分成若干部分,然后从每个部分中选取少数代表性的数据作为测试用例。
-
边界值分析法:就是对输入或输出的边界值进行测试的一种测试设计方法。通常边界值分析法是作为对等价类划分法的补充,这种情况下,其测试用例来自等价类的边界。
-
错误推测法:在测试程序时,人们可以根据经验或直觉推测程序中可能存在的各种错误,从而有针对性地编写检查这些错误的测试用例的方法。
-
因果图法:是从自然语言书写的程序规格说明的描述中找出因(输入条件)和果(输出或程序状态的改变),通过因果图转换为判定表。
-
判定表法:是分析和表达多逻辑条件下执行不同操作的情况的工具。
-
正交试验法:是研究多因素多水平的又一种设计方法,它是根据正交性从全面试验中挑选出部分有代表性的点进行试验,这些有代表性的点具备了“均匀分散,齐整可比”的特点,正交试验设计是分式析因设计的主要方法。是一种高效率、快速、经济的实验设计方法。
-
功能图法:是一种灰盒测试(因其兼有黑盒和白盒测试)方法;通常情况一个程序的功能说明通常由动态说明和静态说明组成;动态说明描述了输入数据的次序或转移的次序,静态说明描述了输入条件与输出条件之间的对应关系。用功能图形象地表示程序的功能说明,并机械地生成功能图的测试用例。
-
场景法:核心思想是围绕用户可能进行的任务流或者业务流程来设计测试用例。它通过模拟用户在实际应用中的各种典型操作,确保软件的各个功能模块能够在真实的使用场景中正常工作。
(二)选择合适的测试用例设计策略
-
首先进行等价类划分,包括输入条件和输出条件的划分,将无限测试变为有限测试,这是减少工作量和提高测试效率的最有效方法。
-
在任何情况下都可以使用边界值分析方法。
-
可以使用错误推测法追加一些测试用例,这需要测试工程师的智慧和经验。
-
如果程序的功能说明书中含有输入条件和输出条件的组合情况,则一开始就可选用因果图法和判定表驱动法。
-
对于参数配置类软件,要使用正交试验法选择较少的组合方法达到最佳效果。
-
功能图法也是也是很好的测试设计方法,我们可以通过不同时期条件的有效性设计不同的测试数据。
-
对于业务流程清晰的系统,可以利用场景法贯穿整个测试用例设计过程。
-
在实际项目中可综合使用各种测试用例设计方法。
(三)命名能够清晰的描述需要测试的业务场景
类命名规则:测试类与被测类的命名应保持一致,通常情况下,测试类的名称为:被测类名称+Test后缀。例如这里的Game类为被测类,则测试类命名为GameTest。
方法命名规则:测试方法应表达业务含义,这样就能使得测试类可以成为文档,测试方法可以足够长,以便于清晰地表述业务。
为了更好地辨别方法名表达含义,Thoughtworks提倡用Ruby风格的命名方法,即下划线分割方法的每个单词或者Java传统的驼峰风格。
(图9 测试方法命名示例图)
(四)Given-When-Then模式编写测试用例
在编写测试方法时,应遵循Given-When-Then模式,这种方法描述了测试的准备,期待的行为,以及相关的验收条件:
Given:为要测试的方法提供准备,包括创建被测试对象,为调用方法准备输入参数实参等;
When:调用被测试的方法,遵循SRP原则,在一个测试方法的when部分,应该只有一条语句对被测方法进行调用。
Then:对调用后的结果进行预期验证。
(图10 Given-When-Then模式示例图)
五、结论语
在开发阶段选择合适的测试策略、保障测试的稳定性及编写高质量的测试,对于减少维护成本、保障测试质量和提升反馈效率至关重要。本文研究了测试策略选择的重要性,同时,高质量的测试编写的原则和最佳实践也被深入探讨,以提高测试覆盖率和有效性。
本研究为软件工程领域提供了新的见解,特别是在保障测试质量和提升反馈效率方面。未来研究方向包括进一步探索自动化测试工具和框架,以提高测试效率和准确性,并研究如何将人工智能和机器学习技术应用于测试过程中,以实现更智能、更高效的测试。此外,未来的应用场景研究可以关注如何将测试策略选择和测试编写应用于不同类型的软件开发项目,以满足不同项目的需求。
总之,选择合适的测试策略、保障测试的稳定性及编写高质量的测试是软件开发过程中不可忽视的关键环节。通过深入研究和实践,我们能够提高软件质量,降低维护成本,提升反馈效率,为软件工程领域带来更大的价值。
参考文献:
[1]Martin Fowler. https://martinfowler.com/
[2]James Whittaker、Jason Arbon、Jeff Caro. Google软件测试之道 [M] 人民邮电出版社,2013