现代软件测试技术之美

978-7-115-62259-4
作者: 茹炳晟
译者:
编辑: 张涛

图书目录:

详情

本书内容聚焦于“现代”软件测试技术,既包括近几年颇受关注的前沿软件测试技术,也包括一些“老技术”在新场景下的应用。作者希望将这些技术剖析清楚,在此基础上给出一些典型的实践案例或应用场景,让读者深入理解这些软件测试技术的来龙去脉,并能够将其快速应用到实践中。本书分为7章,主要内容包括软件测试新理念、软件测试新方法、软件测试新技术、软件测试基础设施、软件测试常见困惑、软件测试行业案例等。   本书内容通俗易懂,案例丰富,既适合软件测试从业人员(测试工程师、测试开发工程师、测试架构师、测试经理、测试总监等)阅读,也适合软件开发人员、架构师和企业管理人员阅读,还适合作为高等院校相关专业的教学用书。

图书摘要

版权信息

书名:现代软件测试技术之美

ISBN:978-7-115-62259-4

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

版  权

编  著 茹炳晟  吴骏龙  刘 冉

责任编辑 张 涛

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

内 容 提 要

本书内容聚焦于“现代”软件测试技术,既包括近几年颇受关注的前沿软件测试技术,也包括一些“老技术”在新场景下的应用。作者希望将这些技术剖析清楚,在此基础上给出一些典型的实践案例或应用场景,让读者深入理解这些软件测试技术的来龙去脉,并能够将其快速应用到实践中。本书分为7章,主要内容包括软件测试新理念、软件测试新方法、软件测试新技术、软件测试基础设施、软件测试常见困惑、软件测试行业案例等。

本书内容通俗易懂,案例丰富,既适合软件测试从业人员(测试工程师、测试开发工程师、测试架构师、测试经理、测试总监等)阅读,也适合软件开发人员、架构师和企业管理人员阅读,还适合作为高等院校相关专业的教学用书。

业界人士推荐

本书全面介绍了现代软件测试技术、工具与实践,例如,契约测试、混沌工程、流量回放、精准测试等。书中介绍的可测试性设计、TDD、实际案例等内容也很实用。全书涵盖了新的测试技术和测试场景,实践性强,有助于快速提高读者的技术水平和实践能力。

——朱少民,《全程软件测试》《敏捷测试:以持续测试促进持续交付》的作者

本书系统地剖析了现代软件测试所面临的诸多挑战,针对性地总结了测试左移、右移的理念和实践,也提炼了在安全和大数据等新兴测试方向上的实战方法,同时还展望了生成式AI在智能测试方向的应用。该书内容广泛、案例实用,是测试从业人员很好的参考书。

——肖然,Thoughtworks中国区总经理、中关村智联软件服务业质量创新联盟秘书长

当我第一次拿到这本书时,就被这本书的内容深深吸引了。这是一本涵盖测试技术、质量保障实践的书,不仅适合有一定经验的从业者参考,而且也适合初入测试行业的读者学习,如果你也和我一样热爱软件测试技术,那么,你一定不要错过这本书。

——陈磊,京东前测试架构师、《接口测试方法论》《持续测试》的作者

本书深入浅出地探讨了现代软件测试的技术与实践,涵盖了新的测试理念和测试场景,如人工智能辅助测试、混沌工程等。本书还包含丰富的案例,帮助读者将理论知识与实际工作相结合。我向每一位追求卓越、渴望在软件测试领域持续成长的专业人士,强烈推荐这本不可多得的佳作。

——阮峰,南京争锋信息科技有限公司董事长兼CEO

本书不但介绍了测试左移和测试右移的基本理论,而且也阐述了当前主流的软件测试技术和新的应用场景,非常适合希望在软件测试领域持续提升能力的读者学习。本书还特别介绍了最近正在兴起的各种测试新技术,如混沌测试、精准测试、全链路压测、大数据测试、人工智能测试等,助力读者解决传统测试技术效率不高和无法发现更多软件Bug的问题,对读者具有很好的实践指导作用。

——徐琨,Testin云测CEO

前  言

在当今快速发展的软件行业中,软件测试是确保软件产品质量的关键环节。然而,随着软件开发的不断变化,软件测试也面临着许多挑战和难点。例如,如何在快速迭代的开发周期中保证测试的全面和深入?如何在复杂的软件架构和技术栈中进行有效的测试?如何在保证测试质量的前提下有效控制测试成本?如何在人工智能(Artificial Intelligence,AI)等新技术的加持下提升测试人员的工程能力?

本书就是为了应对这些挑战和解决工程难点而编写的。本书全面、系统地介绍现代软件测试技术的各种理念与实践,同时提供丰富的案例,使读者能够快速将所学的知识应用到实践中。

在本书中,我们深入研究软件测试中的挑战和难点,并结合自身多年的从业经验,总结出一些应对这些挑战和解决工程难点的有效方法。

首先,针对快速迭代的开发周期,我们践行测试左移和测试右移的理念。测试左移是指在开发过程中,测试人员尽早介入,与开发人员一起完成测试用例的编写和执行;测试右移则是指在产品上线后,测试人员通过实时监控和反馈,对产品进行持续的测试和优化。通过测试左移和测试右移的结合,测试人员可以在快速迭代的开发周期中全面和深入地完成测试工作。

其次,针对复杂的软件架构和技术栈,我们重点强调可测试性的理念和实践。可测试性是指软件产品包括的可理解性、可维护性等。提高软件产品的可测试性,可以使测试人员更加高效地进行测试,并且让自动化测试的成本更低,同时提高软件产品的质量。

最后,针对人工智能等新技术的快速发展,我们讲解人工智能辅助的测试方法和技术,并介绍ChatGPT在软件测试领域的使用场景。人工智能测试是指利用人工智能等技术,对软件产品进行测试和优化的过程。我们运用人工智能测试可以提高测试的效率和准确性,同时能够发现一些采用传统测试方法难以发现的问题。

除了上述内容,本书还全面、系统地介绍契约测试、探索式测试、低代码测试等新方法,以及流量回放、精准测试等“老技术”在新场景下的应用。这些方法和技术的应用,可以更好地解决软件测试的难点。

本书内容立足于软件测试,但不拘泥于软件测试技术本身,而是跨越了众多技术领域。通过阅读本书,读者将能够更好地理解软件测试的本质和意义,掌握相关的测试技术和方法,从而在实践中取得更好的成果。

最后,我们要感谢师江帆和江菊为本书贡献的实践案例;感谢人民邮电出版社编辑的支持和帮助,没有他们的支持和耐心指导,本书不可能如期出版;感谢我们的家人、朋友和同事们,他们的鼓励和支持一直是我们前进的动力。本书编辑的联系邮箱为zhangtao@ptpress.com.cn。

书中部分英文缩略词介绍:BA是Business Analys的缩写,中文意思是业务分析;SM是ScrumMaster的缩写,中文意思是团队的导师或组织者;PO是Product Operation的缩写,中文意思是产品运营;PM是Project Manager的缩写,中文意思是项目经理;PD是Product Design的缩写,中文意思是产品设计;Dev是Development的缩写,中文意思是开发;QA是Quality Assurance的缩写,中文意思是质量保证;UX是User Experience的缩写,中文意思是用户体验;CI是Continuous Integration的缩写,中文意思是持续集成; CD是Continuous Deployment的缩写,中文意思是持续部署。

茹炳晟 吴骏龙 刘 冉

第1章 软件测试新理念

1.1 测试左移

1.1.1 传统瀑布模型下软件测试的挑战

在早期传统的软件开发流程中,很多项目都是参考瀑布模型来进行开发的。瀑布模型的主要实践是将软件研发全生命周期中的各个阶段——需求分析、架构设计、实现设计、代码开发、单元测试、集成测试、系统测试、上线发布、生产运维,依次排列(如图1-1所示),按顺序执行,即大规模集中的测试工作在软件功能设计与开发完成后才开始。

图1-1 瀑布模型下的软件研发全生命周期

这种模型最大的问题在于,很多软件缺陷其实是在研发早期就引入的,但是发现缺陷的时机往往会大幅度延后。缺陷发现得越晚,定位和修复缺陷的成本就越高。在系统测试阶段发现缺陷后修复的成本,大约是在代码开发阶段发现该缺陷后修复成本的40倍,或大约是在单元测试阶段发现该缺陷后修复成本的数倍,Capers Jones关于效能与质量的全局分析(如图1-2所示)直观地表明了这个观点。

图1-2 Capers Jones关于效能与质量的全局分析

根据Capers Jones的统计,大约85%的缺陷是在代码开发阶段引入的,但是因为这个阶段缺乏测试活动,所以发现的缺陷数量几乎为零。而到了软件研发的中后期,由于测试活动的集中开展,缺陷才被大范围发现,但此时的修复成本已变得非常高。

比如,在代码开发阶段引入的缺陷,以及影响接口的缺陷,要等到集成测试阶段才有可能被发现,影响用户界面和用户体验的缺陷要等到系统测试阶段才能被发现,这时返工(rework)的闭环周期被拉得特别长,这样定位问题(缺陷)、修复问题、回归测试的成本就会很高。

让情况变得更糟糕的是,Capers Jones的统计还是基于比较乐观情况的分析,因为其假定软件在研发过程中总是被严格开展单元测试和集成测试,但实际情况是,很多团队寄希望于通过最后的系统测试来发现所有问题,单元测试和集成测试往往会被“偷工减料”,这进一步产生了缺陷发现滞后的问题。

1.1.2 测试左移的早期实践

为了解决缺陷发现滞后的问题,最早的测试左移(shift-left testing)概念被提了出来,此时的测试左移倡导各个测试阶段对应的测试活动应该尽早开展,测试工程师应该在开发提测前就介入,同时将测试活动前移至软件研发生命周期的早期阶段。具体来讲主要包含以下3方面的实践:

加强单元测试,并且对单元测试的覆盖率提出门禁要求,代码的实现问题尽可能在单元测试阶段都被发现;

在开展集成测试前,增大接口测试的占比,接口缺陷尽可能在接口测试阶段被发现;

将集成测试和系统测试的设计与分析工作前置,与实现设计、代码开发阶段并行开展。

测试左移的早期实践,可以提前发现部分缺陷,降低研发过程的不确定性和风险,具体效果如图1-3所示。

图1-3 测试左移早期实践的效果

随着实践的深入,我们发现,如果能有效控制代码开发阶段代码本身的质量,就能更好地实现质量内建。为此,我们在原有实践的基础上增加了以下3方面的实践:

在流程上增加需求解读与评审环节,避免存在需求理解的偏差和不完备性,争取一开始就把业务领域的问题理解透彻,避免后期返工;

在代码开发阶段引入静态代码检查机制,并且不断优化静态代码的扫描规则,将常见问题、代码“坏味道”、安全隐患和性能隐患逐步纳入扫描范围;

贯彻执行代码评审(code review)机制,同时确保避免代码评审的形式主义,并针对代码评审时发现的典型问题在开发团队内形成闭环学习机制。

以上3方面的实践,可以实现缺陷发现的进一步前置以及缺陷数量的降低。实施之后的效果如图1-4和图1-5所示。

如果你以为上面就是测试左移的全部,那你就把事情想简单了。其实这只是测试左移的“冰山一角”。

图1-4 代码开发阶段的质量内建效果(缺陷发现的进一步前置)

图1-5 代码开发阶段的质量内建效果(缺陷数量的降低)

1.1.3 当前软件测试工程化的困局与解法

可能你已经发现前述的测试左移是完全基于瀑布模型的,但是现在,敏捷开发和持续交付等研发模式被广泛采用,再加上软件架构的持续复杂化,前述的测试左移只能在局部范围内发挥作用,我们需要探索并实践适应新时代软件研发模式的测试左移。为此,我们有必要先系统地探讨一下当前软件测试工程化的困局,理解困局将有助于我们对测试进行优化。

总体来看,当前软件测试工程化的困局主要表现在以下3个维度:

技术实现上,软件架构的复杂度越来越高;

团队管理上,开发团队和测试团队的协作成本因为“筒仓效应”变得越来越高;

研发模式上,敏捷开发、持续交付、DevOps等的实践对测试活动提出了全新的要求。

接下来,我们依次展开讨论。

1. 技术实现维度上的困局与解法

从技术实现维度来看,软件架构的复杂度越来越高,软件本身的规模越来越大,传统的测试模式越来越“力有不逮”。

早期的软件基本采用单体架构,通过后期基于黑盒功能的系统测试基本能够保证软件质量。但是如今的软件架构普遍具有冰山模型(如图1-6所示)的特征,基于黑盒功能的系统测试往往只能对水面上的一少部分GUI(Graphical User Interface,图形用户界面)进行验证,大量的业务逻辑实现其实都在水面以下的微服务中,想通过水面上的GUI部分覆盖水面下的所有业务逻辑几乎是不可能完成的任务,因为你可能都不知道水面下有什么。试问,在传统黑盒测试模式下,又有多少测试工程师能够对被测软件的架构设计、调用链路、数据流状态等有清晰的理解呢?

图1-6 冰山模型

现在互联网产品的后端往往非常庞大和复杂,一般由几十到几千个微服务相互协作共同完成前端业务请求,这时候如果把测试寄希望于面向终端用户的系统测试,那么你能够发现的缺陷就会非常有限,而且发现缺陷之后,在调用链路中定位到出问题的微服务的成本也会很高。

在这种情况下,最优的测试策略就是先保证后端每个微服务的质量,这样集成场景下没有问题的概率就能大幅度提高。这就要求测试工作必须前置到微服务的接口开发层面,把大量的组合逻辑验证交由接口测试来覆盖,在GUI层只做基本的业务逻辑覆盖即可。

由上面的分析可以看出,软件架构后端的复杂化对测试的介入时机提出了新的要求,随着微服务架构的发展,测试重点必须从GUI端逐渐左移到API端,此时测试工程师的能力也必须随之扩展,其已经不能完全基于黑盒功能来设计测试用例,而必须知道更多架构和接口设计上的细节才能有效开展测试用例的设计,这些都要求测试介入的时机必须提前,即左移到架构设计。

2. 团队管理维度上的困局与解法

从团队管理维度来看,开发团队和测试团队的协作成本因为筒仓效应变得越来越高,继续采用独立测试团队和开发团队的做法越来越行不通,我们可以通过实际工作中常见的真实例子来感受一下这个困局。

在开发和测试采用独立团队的情况下,当测试工程师发现一个缺陷时,他要做的第一件事就是把缺陷的详细情况了解清楚并且完整记录在缺陷报告中。他需要找到最简单的可稳定重现缺陷的操作步骤,并且需要提供相关的测试数据,还需要对出现缺陷时的软件版本号、环境细节、配置细节等都做详尽的记录。更进一步地,为了便于开发工程师重现缺陷,他最好把出问题时的日志以及相关截图都保留好,一同记录至缺陷报告。这样一份高质量的缺陷报告往往需要花费测试工程师不少的时间。

但是当这份缺陷报告被提交后,如果缺陷不是来自生产环境,那么开发工程师往往并不会立马处理缺陷,因为开发工程师一般会选择确保能够连续完成当前负责的工作,尽量避免被打断。一般过了大半天或一两天,等开发工程师负责的工作告一段落后,他才会开始处理这个缺陷。此时他要做的第一件事就是重现缺陷,在重现缺陷之前,他必须按缺陷报告提供的详细信息重建测试环境,其中包括环境安装、测试数据构建等一系列步骤,所以往往也要花费不少时间。

如果问题能够重现,则可以进一步定位问题;如果问题不能重现,则可能这个缺陷在流程上就要被打回去加以复现。假定现在问题能够重现了,你会发现修复缺陷的过程往往是很快的,因为这个坑就是开发工程师自己挖的。

修复缺陷以后,开发工程师提交代码,集成流水线会生成对应的待测版本并通知之前的测试工程师进行验证。但是此时测试工程师大概率在处理其他测试工作,测试工程师同样不希望被打断,所以不会为了验证这个缺陷立马搭建环境。从效率角度出发,测试工程师一般会选择将多个缺陷集中在一个版本上一起验证,这样就能省掉很多环境搭建的时间。所以从缺陷修复完成到测试工程师验证这个缺陷,往往需要等好几天。

从上面的过程描述中我们可以发现,从缺陷被发现到缺陷最终被验证的整个过程中,真正有效的工作时间占比很小,大量的时间都被流程上的等待和环境安装等耗费掉了。整个过程中,开发工程师没有偷懒,测试工程师也没有偷懒,他们各自都选择了效率最高的方式开展工作,但是从全局视角来看,效率仍十分低下。据一些企业内部的不完全统计,每个缺陷全生命周期中一般会有超过80%的时间被跨团队的过程流转浪费掉。

除了上述时间上的浪费,还有以下原因使得测试团队与开发团队各自独立的组织设置越来越寸步难行。

(1)独立的测试团队往往在开发后期才介入,很难有效保证测试的覆盖率和质量。

(2)开发测试比持续增长,测试人力投入越来越大,实际收益却很低,测试团队进行的测试活动并不能显著降低现网问题数。

(3)需求本身会不断变化,需求的实现也会随之变化,开发团队和测试团队之间的需求传递效率往往十分低下,这在增加漏测隐患的同时,也增加了交接成本。

(4)如果开发团队要快速迭代软件版本,这就要求测试团队具有很高的效率和很短的反馈周期,独立的测试团队很难跟上快速迭代的版本需要。

(5)独立的测试团队有点像“保姆”,这直接导致开发团队的自测意识不够,心理上依赖测试团队,使得质量内建形同虚设。

(6)由于开发团队不负责测试活动,可能未积极考虑如何降低测试的难度,可测试性设计甚至不会被纳入开发工程师的考虑范围。

所以,试想一下,如果测试工程师和开发工程师是同一批人,过程流转造成的大量时间浪费是不是就不会发生?测试活动是不是就能提前介入?需求变化的传递是不是就会更加顺畅?测试的反馈周期是不是也会进一步缩短?开发团队的质量意识是不是也会增强?可测试性问题是不是自然会被纳入开发工程师的考虑范围?这也就是现在先进的软件组织广泛推崇开发者自测的原因,而开发者自测可以说是测试左移的一种有效落地途径,能够最大程度满足质量内建的各种要求。

3. 研发模式维度上的困局与解法

从研发模式维度来看,敏捷开发、持续交付和DevOps等研发模式愈发流行,产品的研发节奏越来越快,传统的“开发提测之后进行测试,然后上线发布”的测试模式面临很大的挑战。在当前新的研发模式下,研发生命周期中的各个阶段(比如设计、开发和测试阶段)都被弱化,或者说边界变得非常模糊,一个迭代通常就包含设计、开发、测试和发布的全流程,已经很难有大把的时间专门用来集中开展测试活动,工程师的能力边界正在变得模糊,普遍需要全栈工程师。

在这种背景下,必须把测试实践全程融入研发的各个阶段,把控各个阶段的质量,而不能依赖于最后的系统测试。我们需要转变观念,传统研发模式下的系统测试以发现问题为主要目标,而现在的系统测试应该以“成果展示”和“获取信心”为主要目标。

1.1.4 测试左移的进阶实践

为了系统性解决软件测试工程化的困局,我们需要重新审视测试左移的原则与实践,在原有测试左移实践的基础上加入新的原则和实践。新时代的测试左移给整个软件测试体系带来了理念上的转变,软件测试不仅仅是在研发过程中发现缺陷,更要致力于在研发全过程中有效推行质量内建,把软件测试活动升级为软件质量工程。为此,我们需要引入以下测试左移的原则和实践。

1.软件质量全员负责制

软件质量全员负责制也可以称为“利益绑定”,这是很关键的一条原则,属于底层逻辑的范畴。在体制设计上,必须让整个研发团队共同对软件产品质量负责,毕竟软件质量不是测出来的,而是开发出来的。如果软件质量出现问题,应由整个研发团队共同负责,而不是让测试团队“背锅”,这种认知上的进步与变革是测试左移能够顺利推行的基本前提。

我们知道,保姆型团队对组织成长是有害的,只有支持型团队才能够发挥更大的价值,从而推动组织更好地成长。当软件质量由整个研发团队共同负责的时候,测试团队就能完成从保姆型团队向支持型团队的蜕变。

2.把测试前置到需求分析和方案设计阶段

在测试左移的早期实践中,测试活动已经被前置到开发阶段,我们可以进一步把测试前置到需求分析和方案设计阶段。这样,测试人员除了能够深入理解需求,在前期掌握详实的需求信息,更重要的是还能够及时评估需求本身的质量,比如分析需求的合理性及完整性等。这样后续的测试分析与测试设计才能有的放矢地开展,实现测试用例先行,争取一次性把事情做正确,避免产生“信息孤岛”以及由此产生的各种潜在返工。

这里推荐使用行为驱动开发(Behavior Driven Development,BDD)和特性驱动开发(FeatureDriven Development,FDD)的方法,在进行需求评估时更多地从测试视角去思考问题,按照编写用户故事或用户场景的方式,从功能使用者的视角描述并编写测试用例,从而让产品经理、开发人员和测试人员着眼于代码所要实现的业务行为,并以此为依据通过测试用例进行验证。

当然,以上实践对测试人员也提出了更高的要求,他们必须掌握行为驱动开发、特性驱动开发、领域驱动设计(Domain Driven Design,DDD)以及实例化需求(Specification By Example,SBE)等技能。

3. 鼓励开发人员自测

一方面开发人员必须对软件质量负责,另一方面测试活动正在不断渗透到开发的各个阶段,同时对测试人员的技能要求越来越向开发人员看齐,由开发人员自己来承担测试工作的诉求正变得越来越强烈。我们需要的不再是独立的开发人才和测试人才,而是全栈型人才。在这种大背景下,开发者自测就变得理所应当了。我们要将传统职能型团队重组为全栈团队,不然质量左移、质量内建只会流于表面。

但是说到让开发人员自己完成测试工作,我们常常会听到很多质疑声,质疑的焦点是开发人员是否适合做测试,这里我们展开讨论一下。

从人性的角度来看,开发人员通常具备“创造性思维”,自己开发的代码就像亲儿子一样,怎么看都觉得很棒;而测试人员则具备“破坏性思维”,测试人员的职责就是尽可能多地找到潜在的缺陷,而且专职的测试人员通常已经在以往的测试实践中积累了大量典型的容易找到缺陷的模式,所以测试人员与开发人员相比,往往更能客观且全面地做好测试。

从技术层面来看,由开发人员自己完成测试,会存在严重的“思维惯性”——通常开发人员在设计和开发过程中没有考虑到的分支和处理逻辑,在自己做测试的时候同样不会考虑到。比如对于一个函数,它有一个 string 类型的输入参数,如果开发人员在做功能实现的时候完全没有考虑到 string类型的参数存在 null 值的可能性,那么代码的实现里面也不会对 null 值做处理,在测试的时候就更不会设计针对null 值的测试数据,这样的“一条龙”缺失就会在代码中留下隐患。

上述分析非常客观,笔者曾经也非常认同,但是在经历并主导了国内外多家大型软件企业的开发者自测转型实践之后,笔者改变了看法。开发人员其实是最了解自己代码的人,所以他们能够最高效地对自己的代码进行测试,开发人员可以基于代码变更自行判断可能受影响的范围,实现高效的精准测试。同时,当开发人员有了质量责任和测试义务之后,测试能力就会成为其技能发展的重要方向。我们说“好马是跑出来的,好钢是炼出来的”,只有通过实战,开发人员的测试分析与设计能力才能提升,进而开发的内建质量才能提升。可以说,开发者自测是软件质量提升的必经之路。

4. 在代码开发阶段借助TDD的思想

这里的TDD是指测试驱动开发(Test Driven Development),但我们并不是要照搬TDD的实践,而是借助TDD的思想,用测试先行的思路,帮助开发人员梳理和理解需求,完成更好的代码设计与实现,缩短代码质量的反馈周期,提高软件质量。

5. 预留测试时间

在做项目计划的时候,尤其在让开发人员进行时间评估时,必须为自测预留时间。一个功能可以交付的标准不仅仅是功能的实现,对应的测试也是需要时间成本的,这需要管理层进行思维转换,否则测试左移只能停留在概念层面,很难真正落地。

6.提高软件的可测试性

提高软件的可测试性是测试左移中一个重要的实践,因为它可以有效地帮助我们设计适合团队的测试策略。我们需要测试人员在参与需求分析和方案设计的过程中,能够提出相关的可测试性需求,帮助研发人员设计出易于测试的软件架构和代码模块,从而提高测试工作的效率和有效性。关于软件的可测试性设计,详见1.3节。

1.1.5 测试左移的深度思考

下面分享笔者的一个感悟:很多时候,我们低估了测试左移的价值。它不仅仅是一种研发模式的改进,更反映了当今软件研发的一个底层逻辑——今天的软件组织,正在由流程驱动转变成事件驱动。

流程驱动是什么呢?是设置一系列的条条框框,从而把研发活动固定为整齐划一的一系列步骤。而高效的软件质量实践能够靠这个来完成吗?答案显然是否定的。

现在的研发活动本质上是靠一个个的事件驱动的,让研发团队以完成事件为指引,充分发挥主动性,充分给予工程师自由。从全局的视角来看,测试左移能被行业所接受,其实反映了软件组织的管理正在向事件驱动的模式转变。

1.1.6 总结

本节探讨了传统瀑布模型和敏捷开发、持续交付、DevOps等研发模式下的测试左移实践,并介绍了开发者自测的实践。

1.2 测试右移

测试右移是指将软件测试的工作扩展至生产环境,确保软件在生产环境中具备正确的功能、良好的性能和稳定的可用性。测试右移的本质思想是将质量管理延续到服务发布后,通过监控、预警等手段,及时发现问题并跟进解决,将影响范围降至最小。

测试右移最典型的理念是TiP(Test in Production,产品测试)。传统观念中,人们普遍认为生产环境是服务于最终用户的,软件产品只有在测试环境下进行充分测试后,才会发布给用户。然而,我们必须接受的现实是,测试环境和生产环境在稳定性保障、部署形式、数据内容等方面都是有差异的,即使能做到没有差异,测试验证点本身也是难以穷举的。换言之,对于软件质量保障,仅仅依靠测试环境中的测试工作是远远不够的,此时基于TiP理念的各项实践就成了很好的补充。

基于TiP理念的实践有生产测试和性能评估、A/B测试、灰度发布、混沌工程、线上监控、用户体验分析等。其中,针对生产测试和性能评估,我们会在4.1节介绍全链路压测的内容,而混沌工程将在2.5节展开介绍,其余4项实践内容,我们在本节做详细介绍。

1.2.1 A/B测试

当前的互联网环境充满了不确定性,一个功能在上线前,我们往往很难预估市场对该功能的反应,此时A/B测试就有了用武之地。A/B测试是一种比较常见的软件发布策略,但它更是一种业务决策手段。如图1-7所示,为用户同时推送新旧版本的功能并进行对比实验,可以分析这一功能给用户带来的价值是否达到预期,并指导下一步的业务决策。简而言之,A/B 测试能快速帮助我们做出正确决策。

图1-7 A/B测试的流量推送策略

A/B测试是一种“先验”的实验体系——通过科学的实验设计、采样具有代表性的样本、流量分割与小流量测试等手段,获得实验结论。A/B测试一般包含以下5个步骤。

(1)确定优化目标:在实施A/B测试之前,我们需要设定明确的优化目标,确保目标是可量化的,否则后续的实验和分析都会无从下手。举个例子,“将用户满意度提升20%”就不是一个合适的目标,因为它太难量化了;而“通过优化运费的展示格式,提升10%的用户留存率(按每月计算)”就是一个合适的目标,因为它既可以被客观量化,又足够具体。

(2)分析数据:以数据分析的方式,找出现有软件产品中的潜在问题,继而挖掘出相应的优化方案。

(3)提出假设:针对上一步发现的问题,提出优化方案。在A/B测试中,这些优化方案一般是以“假设”的方式被提出的,而且往往会提出多个假设。例如,“假设降低5%的运费,用户留存率可能会提升10%”“假设优化运费的展示格式,用户留存率也可能会提升10%”。基于这些假设制定A/B测试的实验方案,并根据实验结果判断是否符合预期。

(4)进行重要性排序:由于我们提出了较多假设,实际情况下受资源限制很难对这些假设一一进行验证,此时就需要对这些假设进行重要性排序,根据资源成本优先验证最重要的假设。

(5)实施A/B测试并分析实验结果:基于选取的重要假设,实施A/B测试,并得出实验结果。若实验结果证明假设成立,则可以考虑将这一功能版本作为正式版本推送给所有用户;若实验结果证明假设不成立,就进行复盘、积累经验。

在工程领域,已有不少工具能够支撑A/B测试的整个体系,比较著名的开源工具有GoogleOptimize 360,也有一些商用化的A/B测试工具,如Optimizely、AppAdhoc等。如果企业有较强的定制化需求,还可以考虑自研A/B测试工具。

1.2.2 灰度发布

灰度发布又称“金丝雀发布”,是一种在新旧版本间平滑过渡的发布方式。它起源于采矿工人的实践经验,金丝雀对瓦斯气体非常敏感,瓦斯浓度稍高就会中毒,采矿工人在探查矿井时,会随身携带一只金丝雀,如果金丝雀的生命体征出现异常,就意味着矿井中存在瓦斯浓度过高的风险。

灰度发布背后的理念很简单,用较小的代价(一只金丝雀)去试错,这样即便出现了风险(瓦斯浓度过高),主要的用户群体(采矿工人)也仍然是安全的。从软件工程的角度来讲,如图1-8所示,通过引流的方式让少部分线上用户先接触到新版本功能,同时技术人员在新版本功能上做一些验证工作,观察监控报警,确认功能无误后,逐步将流量切换至新版本上,直至所有流量都切换完毕。

图1-8 灰度发布的引流策略

如果在切换的过程中发现新版本功能有问题,则应该立即将所有流量切回旧版本上,将影响范围降至最小。

灰度发布的技术实现并不困难,方案也比较丰富,较为简单的做法是引入带权重的负载均衡策略,将用户请求按比例转发至新旧版本上。一些开源服务组件支持灰度发布功能的定制,例如,我们可以基于Apache Dubbo中的Router/LoadBalance实现灰度发布功能,也可以在Spring Cloud中基于Ribbon定制实现灰度发布功能,甚至可以直接使用Nginx Ingress在网关层实现灰度发布功能。

灰度发布有如下3种常见的策略。

按流量比例:这是最简单的灰度发布策略,也就是将流量按比例转发至新旧版本上,以达到灰度发布的效果。

按人群特点:根据人群的特点(如用户ID、用户所在地区、用户类型、用户活跃度等)进行导流,以便精准地管控灰度范围。

按渠道:根据不同渠道(如注册方式、手机运营商、App平台等)进行导流,这也是一种精确的灰度发布策略。

最后,我们有必要谈一下灰度发布策略的一些常见误区,以帮助读者举一反三。

以偏概全:选择的灰度范围不具备代表性,比如我们上线了一个针对会员的新功能,但选择的灰度发布策略所覆盖的大部分用户不是会员,这就大大影响了灰度发布发现问题的能力。

无效灰度:灰度的本质是提前试错,但前提是有能力试错。笔者曾经历过一次印象深刻的高级别线上事故,研发人员更改了用户下预约单的逻辑,引入了一个bug,这个bug本应在灰度发布阶段被发现,但遗憾的是,灰度发布时已是当天21点,而灰度发布策略所涵盖的门店恰恰在21点全部关店歇业了,导致没有任何灰度流量触及新功能,研发人员误认为一切正常,最终引发了事故。由此可见,灰度发布策略需要保证新功能一定被验证到,不存在无效灰度的情况。

监控缺失:我们不仅需要有效的灰度发布策略,还需要辅以完备的监控,以便及早发现风险,采取止损措施。

1.2.3 线上监控

实施线上监控的目的是第一时间发现线上问题并解决问题,保证服务的正常运行。线上监控是一个很宽泛的话题,涉及的技术点非常多。在本小节中,我们侧重于讨论基于测试右移的理念,都有哪些监控工作是需要测试人员重视的。我们总结为以下几个要点:

服务上线后的可用性和性能监控,如遇到问题需要快速回滚代码;

持续的服务关键指标监控,出现报警时能够初步定位问题,与研发人员配合实现止损和修复;

对生产数据进行监控,对异常数据及时介入干预;

进行线上资金实时/离线核对,对资损风险及时介入干预;

进行安全性监控,初步识别安全风险;

对用户反馈的问题及时跟进,通知开发人员尽快解决缺陷,通知产品人员打磨细节、提升体验。

对于上述最后一点,我们需要强调的是,线上监控不仅仅针对应用服务,舆情监控同样重要。对于用户反馈的问题,由客服人员初步判断为技术问题后,测试人员(或技术支持人员)要能够及时跟进处理或分流,以便尽可能快速地给予用户有效的反馈。

另外,上述要点并不是单纯的监控工作内容,我们需要将其内化为质量保障的能力,通过工具和规范,赋能各个技术人员共同参与线上监控的工作。例如,我们可以先将日常的监控项明确清楚,设计好相关的质量数据报表,再通过采集监控数据进行分析和配置告警,来观察版本发布的情况,最终建立一个线上质量看板,以便相关人员及时获悉线上质量情况。

1.2.4 用户体验分析

用户体验分析是收集真实用户的反馈,分析数据并总结出系统改进措施的过程。它是测试右移的极致追求,不仅仅满足于软件产品的可用性,还很重视用户的情感、喜好、认知印象、生理和心理反应、行为和成就等各个方面。

用户体验分析中最常见的方法是问卷调查——将精心设计的量表,发放给特定的真实用户,收集反馈并得出结论。下面我们以SUS(System Usability Scale,系统可用性量表)为例,学习一下问卷调查的过程。

如表1-1所示,SUS问卷包含10个题目,每个题目的分值均为5分,奇数项是正面描述题,偶数项是反面描述题。我们要求用户在填写SUS问卷时,不要互相讨论,也不要过多思考,而应尽可能快速地完成所有题目。

表1-1 SUS问卷标准版

序号

问题

非常同意→非常不同意

1

2

3

4

5

1

我愿意使用这个系统

2

我发现这个系统过于复杂

3

我认为这个系统使用起来很容易

4

我认为自己需要专业人员的帮助才能使用这个系统

5

我发现系统里的各项功能很好地整合在了一起

6

我认为系统中存在大量的不一致

7

我能想象大部分人能够快速学会使用这个系统

8

我认为这个系统使用起来非常麻烦

9

当使用这个系统时,我对自己非常有信心

10

在使用这个系统之前,我需要进行大量的学习

收回所有的SUS问卷,统计总分。先确定每道题的转化分值,分值范围为0~4。对于正面描述题,转化分值是量表原始分减去1;对于反面描述题,转化分值是5减去量表原始分。将所有题目的转化分值相加后再乘以2.5,得到SUS问卷的总分。所以SUS分值范围为0~100,以2.5分为增量。

将得到的SUS问卷的总分对应到表1-2,即可得到产品的可用程度,我们可以将其作为用户体验的一个重要参考。

表1-2 SUS问卷总分的曲线分级范围

SUS分值范围

评级

百分范围

84.1~100

A+

96~100

80.8~84

A

90~95

78.9~80.7

A-

85~89

77.2~78.8

B+

80~84

74.1~77.1

B

70~79

72.6~74

B-

65~69

71.1~72.5

C+

60~64

65~71

C

41~59

62.7~64.9

C-

35~40

51.8~62.6

D

15~34

0~51.7

F

0~14

上面介绍的SUS非常实用,但它也有缺点。由于它的评分结果是抽象的,这个分数只能让我们大概了解针对某产品用户体验的好坏,在具体问题上缺乏指引。当我们希望了解产品评分较低时应当如何聚焦产品的优化方向时,SUS就无能为力了。

下面我们介绍一种更通用的用户体验分析方法——雷达图分析法。该方法的实施具体分三步。

第一步,对潜在的用户体验问题进行分类,得到基础的分析项,例如视觉呈现、界面设计、导航设计、信息设计、交互设计、信息架构、功能规格、内容需求等。

第二步,以问卷的形式交由目标用户评估,如表1-3所示。与SUS问卷不同,这些目标用户需要具备一定的可用性分析能力,建议由专家带领讨论,以便解答评估过程中的困惑。

第三步,将问题汇总整理,以雷达图的形式展示出来。

表1-3 用户体验问题记录表

序号

分析项

分值(由低到高为1~5分)

问题描述

1

导航设计

2

导航菜单的嵌套过深,定位到某一页面的操作步骤过多

2

3

如图1-9所示,雷达图能够以直观的形式展现多个维度用户体验问题的整体情况,便于我们全面分析和解读指标,以及一目了然地发现哪些方面存在用户体验问题。

图1-9 用户体验雷达图

1.2.5 总结

测试右移致力于在生产环境或生产阶段进行测试或相关质量保障工作,作为传统测试工作的有力补充,本节对测试右移理念下的A/B测试、灰度发布、线上监控和用户体验分析这4项实践内容进行了解读,并提供了一些实用的方法。

1.3 可测试性设计

随着云原生技术的加速普及与快速发展,软件系统的规模不断扩大,复杂性也不断提高。与此相对应,在软件研发过程中,为测试而设计(design for testing)、为部署而设计(design for deployment)、为监控而设计(design for monitor)、为扩展而设计(design for scale)和为失效而设计(design for failure)正变得越来越重要,甚至成为衡量软件组织核心研发能力的主要标尺。

本节重点探讨“为测试而设计”的理念,以软件的可测试性(testability)为主线,向大家阐述软件可测试性的方方面面,以及软件组织在这个方向上的一些最佳实践与探索。

软件的可测试性对软件的研发和质量保障有着至关重要的作用,是实现高质量、高效率软件交付的基础。可测试性差,会直接增加测试成本,让测试结果验证变得困难,进而导致工程师不愿意做测试,或者使测试活动延迟发生,这些都违背了持续测试、尽早以低成本发现问题的原则。为此,我们有必要对可测试性进行一次深入浅出的探讨,主要内容包含以下5个方面:

可测试性的定义;

可测试性差引发的问题;

可测试性的3个核心观点;

可测试性的4个维度;

不同级别的可测试性与工程实践。

1.3.1 可测试性的定义

软件的可测试性是指在一定时间和成本的前提下,进行测试设计、测试执行,以此来发现软件的问题,以及发现故障并隔离、定位故障的能力。各种组织对可测试性有不同的定义,笔者认为它们的本质是相通的,都是在说软件系统能够被测试的难易程度,或者说软件系统可以被确认的能力。

笔者个人比较喜欢的定义来自James Bach:可测试性就是指一个计算机程序能够被测试的难易程度。

测试设计能力(即创造性地设想各种可能性,并设计相应场景)是每个软件测试人员的核心能力,但是如何根据测试设计构造出所需要的测试条件,如何高效执行测试,以及在测试执行过程中如何对结果进行实时的观察和验证,则是可测试性需要解决的问题。

1.3.2 可测试性差引发的问题

很多觉得可测试性是个新命题,在软件测试发展的很长一段时间里,这个概念似乎并没有被广泛提及。那是因为以前的软件测试是偏粗放式的黑盒模式,而且测试团队和开发团队分离,测试工程师往往到了研发后期才会介入,测试始终处于被动接受的状态,并且大量的测试与验证都偏向黑盒功能,所以可测试性的矛盾并没有显现出来。但是现在,随着测试左移、开发者自测、测试与开发融合以及精准测试的广泛普及,粗放式的黑盒模式已经无法满足软件的质量要求。

如果继续忽视可测试性,不从源头上对可测试性予以重视,将会导致研发过程中系统不可测或测试成本过高的窘境。可以说,忽视可测试性就是在累积技术债务。更何况,今天大行其道的DevOps全程都离不开测试,测试成了拉通持续集成/持续交付(Continuous Integration/Continuous Delivery,CI/CD)各个阶段的连接器,如果可测试性不佳,整个CI/CD的效率就会大受影响。

为了帮助大家更好地理解可测试性,下面列举一些实际的可测试性问题。

1. GUI测试层面

登录场景下的图片验证码:图片验证码虽然不影响手工测试,但是会影响自动化测试的可测试性,用OCR(Optical Character Recognition,光学字符识别)技术识别图片验证码往往不够稳定,如果能够实现稳定识别,反而说明验证码机制有问题。如果登录实现不了自动化,就会影响很多其他的自动化测试场景。登录过程中的短信验证码也有类似的可测试性问题。

页面控件没有统一且稳定的ID标识:如果页面控件没有统一且稳定(不随版本发布而变化)的ID标识,自动化测试脚本中控件识别的稳定性就会大打折扣。虽然测试脚本可以通过组合属性、模糊识别等技术手段来提升识别的稳定性,但是测试的成本会变高。

非标准控件的识别:非标准的前端页面控件等无法通过GUI自动化测试识别出来。

需要对图片形式的输出进行验证:图片的验证缺乏有效的工具来支持。

2. 接口测试层面

接口测试缺乏详细的设计文档:接口测试如果没有设计契约文档作为衡量测试结果的依据,就会造成测试沟通成本高昂,陷入无法有效开展测试结果验证、开发人员和测试人员相互推诿的窘境。

构建Mock服务的难度和成本过高:在微服务架构下,如果构建Mock服务的难度和成本过高,就会直接造成接口不可测或者测试成本过高。

接口调用的结果验证困难:接口成功调用后,判断接口行为是否符合预期的验证点难以获取。

接口调用不具有幂等性:接口内部处理逻辑依赖于未决因素(如时间、不可控输入、随机变量等)破坏接口调用的幂等性。

3. 代码测试层面

私有函数的调用:在代码测试中,私有函数无法直接调用。

私有变量的访问:私有变量缺乏访问手段,以至于无法进行结果验证。

代码依赖关系复杂:被测代码中依赖了外部系统或者不可控组件,比如依赖第三方服务、网络通信、数据库等。

代码可读性差:代码采用了“奇技淫巧”,造成可读性差,同时缺乏必要的注释说明。

代码的圈复杂度过高:圈复杂度过高的代码往往很难设计测试。

4. 通用测试层面

无法获取软件内部信息:测试执行过程中,有些结果的验证需要获取软件内部信息进行比对,如果无法通过低成本的手段获取软件内部信息,测试的验证成本就会很高。

多样性的测试数据的构建:很多测试设计都依赖于特定的测试数据,如果多样性的测试数据的构建比较困难,也会直接影响系统的可测试性。

无法获取系统运行时的实时配置:无法获取系统运行时的实时配置意味着无法重建测试环境进行问题的重现和定位,从而增加了测试的难度与不确定性。

压测场景下的性能剖析:很多性能问题只有在高负载场景下才能重现,但是在高负载场景下,无法通过日志的方式来获取系统性能数据,因为一旦提高了日志等级,日志输出就会成为系统瓶颈,进而把原来的性能问题掩盖掉。

由此可见,可测试性问题不仅出现在端到端的功能测试层面,还出现在接口测试和代码测试层面。可测试性对于自动化测试的实现成本也很关键。

1.3.3 可测试性的3个核心观点

在正式讨论可测试性的技术细节之前,很有必要先介绍可测试性的核心观点。笔者认为可测试性有3个核心观点(如图1-10所示)。

图1-10 可测试性的3个核心观点

1. 可测试性是设计出来的

毋庸置疑,可测试性不是与生俱来的,而是被设计出来的。可测试性必须被明确地设计,

并且正式纳入需求管理的范畴。在研发团队内,测试架构师应该牵头推动可测试性的建设,并与软件架构师、开发工程师和测试工程师达成一致。测试工程师和测试架构师应该是可测试性需求的提出者,并且负责可测试性方案的评估和确认。在研发过程中,可测试性的评估要尽早开始,一般始于需求分析和设计阶段,并贯穿研发全流程,所以可测试性不再只是测试工程师的职责,而是整个研发团队的职责。

2. 提升可测试性可以节省研发成本

良好的可测试性意味着测试的时间成本和技术成本都会降低,还能提升自动化测试的可靠性与稳定性。今天在可测试性上的前期投资,会带来后续测试成本的大幅度降低。今天多花的一块钱可以为将来节省十块钱,这再次证明了“很多时候选择比努力更重要”。

3. 关注可测试性可以提升软件质量

可测试性好的软件必然拥有高内聚、低耦合、接口定义明确、行为意图清晰的设计。在准备写新代码时,要问自己一些问题:我将如何测试我的代码?我将如何在尽量不考虑运行环境因素的前提下编写自动化测试用例来验证代码的正确性?如果你无法回答好这些问题,那么请重新设计你的接口和代码。当你开发软件时,时常问自己如何验证软件的行为是否符合预期,并且愿意为了达成这个目标而对软件进行良好的设计,作为回报,你将得到一个具有良好结构的系统。

要让研发团队重视可测试性是件很难的事情,究其根本原因,在于研发团队“不够痛”。

长久以来,测试团队和开发团队一直是独立的两个团队,开发工程师往往更关注功能的实现,其次才会关注一些类似性能、安全和兼容性相关的非功能需求,可测试性基本是没有任何关注优先级的,因为测试工作并不是由开发工程师自己完成的,可测试性的价值开发工程师往往感受不到。而测试工程师虽然饱受可测试性的各种折磨,却苦于处于软件研发生命周期的后期,对此也无能为力,因为很多可测试性需求是需要在设计阶段就考虑并实现的,到了最后的测试阶段很多事情为时已晚。

很多时候,你不想改是因为你不痛,你不愿意改是因为你不够痛,只有真正痛过才知道改的价值。所以应该让开发工程师自己承担测试工作,这样开发工程师才会切身地感受到可测试性的重要性与价值,进而在设计与开发阶段赋予系统更优秀的可测试性,由此形成的良性循环能让系统整体的可测试性始终处于较高水平。

1.3.4 可测试性的4个维度

可测试性的分类方法有很多不同的版本,比如由James Bach提出的实际可测试性模型(Heuristics of Software Testability,如图1-11所示)、由Microsoft(微软)提出的SOCK可测试性模型(如图1-12所示)、由Siemens(西门子)提出的可测试性设计检查表模型等。

图1-11 由James Bach提出的实际可测试性模型

图1-12 由微软提出的SOCK可测试性模型

虽然各种分类方法的切入点不尽相同,但它们的本质是相通的。在这些模型的基础上,笔者做了一些归纳和总结,将可测试性分成可控制性、可观测性、可追踪性与可理解性4个维度(如图1-13所示)。下面我们依次展开讨论。

图1-13 可测试性的4个维度

1. 可控制性

可控制性是指能否容易地控制程序的行为、输入和输出,以及是否可以将被测系统的状态控制到满足测试条件。一般来讲,可控制性好的系统一定更容易测试,也更容易实现自动化测试。可控制性一般体现在以下几个方面。

在业务层面,业务流程和业务场景应该易分解,尽可能实现分段控制与验证。对于复杂的业务流程,须合理设定分解点,以便在测试时进行分解。

在架构层面,应采用模块化设计,各模块之间支持独立部署与测试,具有良好的可隔离性,以便构造Mock环境来模拟依赖。

在数据层面,测试数据也需要可控制性,这样才能够低成本构建多样性的测试数据,以满足不同测试场景的要求。

在技术实现层面,可控制性的实现手段涉及很多方面,比如提供适当的手段以便在系统外部直接或间接地控制系统的状态及变量,在系统外部实现便捷的接口调用,实现私有函数以及内部变量的外部访问能力,实现运行时的可注入能力,实现轻量级的插桩能力,使用AOP(Aspect-Oriented Programming,面向切面编程)技术实现更好的可控制性等。

2. 可观测性

可观测性是指能否容易地观察程序的行为、输入和输出,一般指系统内的重要状态、信息可通过一定手段从外部获得的难易程度。

任何一项操作或输入都应该有预期的、明确的响应或输出,而且该响应或输出必须是可见的,这里的可见不仅包括运行时可见,还包括维护时可见以及调试时可见,同时在时间维度上应该包含当前和过去都可见,并且是可查询的,不可见和不可查询就意味着不可发现,可观测性就差,进而影响可测试性。

可见的前提是输出,要想提高可观测性,就应该多多输出,包括分级的事件日志(logging)、调用链路追踪(tracing)信息、各种度量指标(metrics),还应该提供各类可测试性接口以获取系统内部信息,以及进行系统内部自检信息的上报,以确保影响程序行为的因素可见。另外,有问题的输出要易于识别,无论是通过日志自动分析还是界面高亮显示的方式,都要能够有助于发现。

关于多多输出的理念,有一个概念性的度量指标DRR(Domain/Range Ratio)可以借鉴。DRR可以理解成输入个数和输出个数的比例。DRR用于度量信息的丢失程度。DRR越大,信息越容易丢失,错误越容易隐藏,可测试性也就越低。因此要降低DRR,在输入个数不变的条件下,增加输出个数,输出参数越多,获取的信息越多,也就越容易发现错误。

接下来我们讨论一下可观测性和监控的关系。监控告诉我们系统的哪些部分不工作了,可观测性则告诉我们那些不工作的部分为什么不工作了,所以笔者认为监控是可观测性的一部分,可观测性是监控的超集。两者的区别主要体现在问题的主动(preactive)发现能力这个层面,可以说主动发现是可观测性能力的关键。可观测性正在从过去的被动监控转向主动发现与分析。

通常我们会将可观测性能力划分为5个层级(如图1-14所示),其中告警(alerting)与应用概览(overview)属于传统监控的概念范畴。触发告警的往往是明显的症状与表象,但随着系统架构复杂度的增加以及应用向云原生部署方式的转变,没有产生告警并不能说明系统一定没有问题。因此,系统内部信息的获取与分析就变得非常重要,这主要通过排错(debugging)、剖析(profiling)和依赖分析(dependency analysis)来实现,它们体现了主动发现能力,并且层层递进:

(1)无论是否发生告警,运用主动发现能力都能对系统运行情况进行诊断,通过指标呈现系统运行的实时状态;

(2)一旦发现异常,逐层下钻定位问题,必要时进行性能分析,调取详细信息,建立深入洞察;

(3)调取模块与模块间的交互状态,通过链路追踪构建整个系统的完整性。

图1-14 可观测性和监控的关系

主动发现能力的作用除了告警与排错,还包括通过获取全面的数据与信息,来构建对系统深入的认知,而这种认知可以帮助我们提前预测与防范故障的发生。

下面我们讨论一下可观测性与可控制性的关系。可观测性不仅能观测系统的输出是否符合设计要求,还影响系统是否可控。系统的必要状态信息在系统测试控制阶段起决定作用。没有准确的状态信息,测试工程师就无法判断是否要进行下一步的状态变更。无法控制状态变更,可控制性又从何谈起?所以可观测性与可控制性是相辅相成的关系,缺一不可。

3. 可追踪性

可追踪性是指能否容易地跟踪系统的行为、事件、操作、状态、性能、错误以及调用链路等。可追踪性有助于你成为“系统侦探”,可以帮助你成为系统的福尔摩斯。可追踪性主要体现在以下几个方面:

记录并持续更新详细的全局逻辑架构视图与物理部署视图;

跟踪记录服务端模块间全量调用链路、调用频次、性能数据等;

跟踪记录模块内关键流程的函数执行过程、输入输出参数、持续时间、扇入扇出信息等;

打通前端和后端的调用链路,实现后端流量可溯源;

实现数据库和缓存类组件的数据流量可溯源;

确保以上信息的保留时长,以便开展以周或月为频次的异常分析。

在云原生时代,综合集成了事件日志(logging)、调用链路追踪(tracing)信息和度量指标(metrics)的OpenTelemetry是可追踪性领域的主要发展方向,OpenTelemetry旨在将logging、tracing和metrics三者统一,实现数据的互通及互操作,以解决信息孤岛的问题。

4. 可理解性

可理解性是指被测系统的信息获取是否容易,以及信息本身是否完备且易于理解。比如被测对象是否有说明文档,说明文档本身可读性和及时性是否都有保证等。常见的可理解性包含以下几个方面:

提供用户文档(使用手册等)、工程师文档(设计文档等)、程序资源(源代码、代码注释等)以及质量信息(测试报告等);

文档、流程、代码、注释、提示信息等易于理解;

被测对象是否有单一且定义清晰的任务,以体现出关注点分离;

被测对象的行为是否可以进行具有确定性的推导与预测;

被测对象的设计模式是否能够被很好地理解,并且遵循行业通用规范。

1.3.5 不同级别的可测试性与工程实践

不同级别有不同的可测试性要求。下面我们分别从代码级别、服务级别和业务需求级别展开讨论。

1. 代码级别的可测试性

代码级别的可测试性是指针对代码编写单元测试的难易程度。对于一段被测代码,如果为其编写单元测试的难度很大,需要依赖很多“奇技淫巧”或者单元测试框架、Mock框架的高级特性,则往往意味着代码实现得不够合理,代码的可测试性不好。如果你是资深的开发工程师,并且一直有写单元测试的习惯,你会发现写单元测试本身其实并不难,反倒写出可测试性好的代码是一件非常有挑战的事情。

代码违反可测试性的情况有很多,常见的有以下这些:

无法Mock依赖的组件或服务;

代码中包含未决行为逻辑;

滥用可变全局变量;

滥用静态方法;

使用复杂的继承关系;

代码高度耦合;

I/O和计算不解耦。

为了便于理解,我们用“无法Mock依赖的组件或服务”给大家展开举个例子,以便大家更好地理解什么是代码级别的可测试性。

被测代码如图1-15所示,Transaction类是经过抽象简化之后的一个电商系统的交易类,用来记录每笔订单交易的情况。Transaction类中的execute()函数负责执行转账操作,将钱从买家的钱包转到卖家的钱包,真正的转账操作是通过在execute()函数中调用WalletRpcService服务来完成的。

图1-15 被测代码——Transaction类

编写一个单元测试,如图1-16所示。

图1-16 编写一个单元测试

这个单元测试的代码本身很容易理解,就是提供参数来调用execute()函数。但是为了让这个单元测试能够顺利执行,还需要部署WalletRpcService服务。一来搭建和维护的成本比较高;二来还需要确保在将伪造的transaction数据发送给WalletRpcService服务之后,能够正确返回我们期望的结果以完成不同执行路径的测试覆盖。而测试的执行需要进行网络通信,耗时也会比较长,网络的中断、超时、WalletRpcService服务的不可用,都会直接影响单元测试的执行,所以从严格意义上来讲,这样的测试已经不属于单元测试的范畴了,更像是集成测试。我们需要用Mock来实现依赖的解耦,用一个“假”的服务替换真正的服务,而且这个假的服务需要完全在我们的控制之下,模拟输出我们想要的数据,以便控制测试的执行路径。

为此,我们构建WalletRpcService服务的Mock(如图1-17所示),这可以通过继承WalletRpcService类,并且重写其中的moveMoney()函数的方式来实现。这样就可以让moveMoney()函数返回任意我们想要的数据,并且不需要真正进行网络通信。

图1-17 WalletRpcService服务的Mock

但是接下来当试图用MockWalletRpcServiceOne和MockWalletRpcServiceTwo替换代码中真正的WalletRpcService时,就会发现因为WalletRpcService是在execute()函数中通过new的方式创建的(图1-15中的第5行代码),我们无法动态地对其进行替换,这就是典型的代码可测试性问题。

为了解决这个问题,需要对代码进行适当的重构,这里使用依赖注入的方式。依赖注入是实现代码可测试性的有效手段之一,可以将WalletRpcService对象的创建反转给上层逻辑,在外部创建好之后,再注入Transaction类。具体的代码实现如图1-18所示。

图1-18 用依赖注入解决代码可测试性问题

这样在单元测试中就可以非常容易地将WalletRpcService替换成Mock出来的MockWalletRpc-ServiceOne或MockWalletRpcServiceTwo了(如图1-19所示)。

图1-19 重构以后的单元测试

在代码级别的可测试性上,Google早期有过一个不错的实践,就是构建了一套工具,名为Testability Explorer,用于专门对代码的可测试性进行综合性的评价并给出分析报告(如图1-20所示),有点类似于代码静态检查的思路。可惜目前Google已经不再继续对Testability Explorer进行维护了。

图1-20 Testability Explorer给出的分析报告

2. 服务级别的可测试性

服务级别的可测试性主要是针对微服务来讲的。相较于代码级别的可测试性,服务级别的可测试性更容易理解。一般来讲,服务级别的可测试性主要考虑以下方面:

接口设计的契约化程度;

接口设计文档的详细程度;

私有协议设计的详细程度;

服务运行的可隔离性;

服务扇入扇出的大小;

服务部署的难易程度;

服务配置信息获取的难易程度;

服务内部状态的可控制性;

测试数据构造难易程度;

服务输出结果验证的难易程度;

服务后向兼容性验证的难易程度;

服务契约获取与聚合的难易程度;

服务资源占用的可观测性;

内部异常模拟的难易程度;

外部异常模拟的难易程度;

服务调用链路追踪的难易程度;

内置测试(Build-In Self Test,BIST)的实现程度。

3. 业务需求级别的可测试性

业务需求级别的可测试性最容易理解,平时大家接触也最多。一般来讲,业务需求级别的可测试性可以进一步细分为手工测试的可测试性和自动化测试的可测试性。业务需求级别的可测试性有以下典型场景:

登录过程中的图片验证码或短信验证码;

硬件U盾/ USB Key;

触屏应用的自动化测试设计;

第三方系统的依赖与模拟;

业务测试流量的隔离;

系统的不确定性弹框;

非回显结果的验证;

可测试性与安全性的平衡;

业务测试的分段执行;

业务测试数据的构造。

1.3.6 总结

本节系统地探讨了可测试性的定义,讨论了可测试性差引发的问题,给出了可测试性的3个核心观点和4个维度,最后从代码级别、服务级别和业务需求级别探讨了可测试性的实例或关注点。

1.4 测试分析与测试设计

1.4.1 什么是测试分析与测试设计

测试分析与测试设计在软件测试中运用十分广泛,也十分重要,是软件测试的基础技能。如果没有测试分析与测试设计,就无法得到有效的、高覆盖的测试用例,从而导致测试工作无法有效完成,测试质量也无法得到有效度量。如果测量质量无法度量,团队和管理人员将难以对软件的质量有信心,更无法发现软件系统的缺陷(defect),从而导致软件系统在线上出现问题的概率大大增加。

1. 测试分析

测试分析是一个分析的过程,它会评估并定义测试的目标和产品的风险,并定义出成功达到目标的评估方法。我们需要通过测试分析明确以下内容:

测试和质量的详细级别;

系统的复杂程度以及开发和发布的流程;

项目和产品的风险;

哪些功能需要测试以及怎么测试;

测试管理如何实施;

测试流程和测试技术;

测试设计的级别以及其他与产品测试和质量有关的事情。

2. 测试设计

测试设计是指针对一个系统以及它的组件设计测试架构和测试用例,比如针对一个特性(feature)、一个交易(transaction)、一个功能(function)等。

设计测试架构,即通过分析被测系统的架构,结合需要实施的不同测试类型,通过架构思维将被测系统、各种类型的测试和它们的测试工具/框架/系统等整合在一起,设计出一套系统的、完整的测试架构。

举例来说,一个前后端分离的Web业务系统不仅有前端UI(User Interface,用户界面)和大量的JavaScript代码,还有后端API、第三方依赖系统以及数据库系统,如何将各层测试有效地联系起来就是测试架构需要解决的问题。

对于不同的软件系统,其测试架构一般是根据业务需求、技术能力等各种条件来设计的。与软件架构一样,测试架构在不同的项目里,需要根据对应软件系统的架构、技术栈、业务需求、人员技能等因素来定制和设计。

而测试用例包括测试的条件、测试的步骤、测试的输入/输出值、测试断言等内容。它是日常工作中,开发人员和测试人员接触最多的部分,也是测试工作基础中的基础。如果它的有效性无法保证,那么整个测试工作的有效性也无法保证。

在实践中,测试分析与测试设计都是并行实施、互相影响的,但是从概念层面来说,它们仍然是两个不同的概念。测试策略的制定,也主要依赖于测试分析与测试设计的产出,由此可见测试分析与测试设计的重要性。

在实施测试分析与测试设计之后,测试策略也就随之产生,然后就是制定测试计划,持续迭代地实施测试分析与测试设计,并行地实现并执行测试用例。

1.4.2 测试分析与测试设计的分类

在传统的软件测试中,测试分析与测试设计分为黑盒分析与设计和白盒分析与设计两类。时至今日,这样的分类可以继续使用,因为它们依然可以帮助我们更容易地归类和理解各种测试分析与测试设计技术。

1. 黑盒分析与设计技术

黑盒测试是最常见的测试类型,传统的测试主要由专职的测试人员来实施,其中最为常用的黑盒分析与设计技术包括以下6种。

(1)等价类

等价类是一种设计技术,可以有效地减少测试用例的数量,并且可以维护一个合理的测试覆盖率。这是一种十分简单的设计技术,所有的开发人员和测试人员都应该掌握。

等价类分析与设计的步骤很简单:首先分析并确认有多少种等价类,然后针对每一种等价类设计一个测试用例。

由于等价类可以用在各种不同类型的测试上,并且性价比很高,因此它是最常用也是最重要的一种测试分析与测试设计技术。

(2)边界值

边界值是一种比较基础的测试分析与测试设计技术,旨在帮助测试人员针对一些存在边界值的业务进行合理的测试覆盖,从而减少问题逃逸。

(3)决策表

决策表是一种非常优秀的测试分析与测试设计技术,它可以将一些变量非常多的复杂业务需求进行系统化的分析和组织,高效地帮助测试人员设计出业务测试覆盖率高的测试用例。虽然非常优秀,但是人们并不常使用它,导致问题逃逸增加。

(4)Pairwise

Pairwise是测试分析与测试设计技术中最神奇也是许多测试人员最不熟悉的一种技术。它采用了一种特定的算法,既能将巨量的、由排列和组合生成的测试用例减少到一个合理的数量,又能最大可能地保证合理的测试覆盖率。

Pairwise可以有效地降低测试用例的数量,但是可能会造成少量的问题逃逸。尽管如此,在测试用例非常多的情况下,Pairwise仍是性价比最高的选择。

(5)状态转换

状态转换和决策表类似,也是一种非常好的复杂系统分析和设计技术。它主要针对系统的各种状态和状态转换进行分析,并画出状态转换图,针对这些状态和它们之间的转换的不同排列和组合进行测试用例的设计。

在存在大量状态和状态转换的系统中,一定要通过这种简单但高效的技术来对系统进行分析并设计测试用例。

(6)领域分析

如今软件系统的业务越来越复杂,测试分析与测试设计也越来越复杂。随着领域驱动开发的出现,软件开发也已进行了领域拆分。与之对应的是测试分析与测试设计也需要进行领域拆分,针对不同的领域进行不同的分析和设计,并且将相同领域里的各种业务需求集中起来进行分析,测试用例也集中起来进行设计和管理,从而对复杂系统的测试用例分析和设计进行拆分,化繁为简。

2. 白盒分析与设计技术

相较于传统的测试类型,在将测试工作左移到开发过程中,且开发人员实施测试工作后,白盒分析与设计开始兴起。它主要由开发人员来实施,在TDD(Test Driven Development,测试驱动开发)中最常见。在测试人员实施ATDD(Acceptance Test Driven Development,验收测试驱动开发)的时候,软件技术较好的测试人员也会尝试通过白盒分析与设计技术来得到更全面的验收测试用例。白盒分析与设计技术的类型相对黑盒分析与设计技术少一些,主要包括以下两种类型:

控制流;

数据流。

不管是基于黑盒的还是基于白盒的测试分析与测试设计技术,本质都是帮助开发人员和测试人员完成业务功能的测试,且它们最终需要完成的都是基于用户行为和业务场景的测试用例或测试场景设计。想要实施良好的TDD的开发人员,不仅要熟悉白盒分析与设计技术,也要熟悉黑盒分析与设计技术,因为在编写一些复杂功能的单元测试时,也需要用到等价类、边界值等基础的测试分析与测试设计技术。而对于测试人员,熟悉各种基础的黑盒测试和白盒测试技术,则是设计出好的基于用户行为和业务场景的测试用例或测试场景的基础能力。

1.4.3 基于用户行为和业务场景的测试分析与测试设计

绝大多数现代商用软件是通过用户的特定行为来完成特定的业务场景的,对于这些商用软件来讲,大部分的测试工作则是通过对用户行为和业务场景进行测试分析,设计并完成基于用户行为(或操作)的业务测试场景来实施的。所以理解用户行为和业务场景,成了现在大部分商用软件的重中之重。

虽然很多测试人员认识到了这一点,但是往往还存在一个误区,就是认为只需要理解业务场景和用户行为,不需要熟悉基础的测试分析与测试设计技术,就可以设计好测试用例和测试场景。但现实情况是,如果不懂基础的测试分析与测试设计技术,往往在分析和设计复杂的业务场景时,不是测试用例数量巨大(比如不使用Pairwise和等价类,而是测试所有组合用例),就是无法分析清楚所有的状态和边界值,导致遗漏一些测试场景。

另外,用户行为分为业务行为和操作行为。传统软件测试以操作行为为主体来进行测试分析与测试设计;而基于复杂业务场景的现代商用软件测试,则需要以用户的业务行为为主体来进行测试分析与测试设计。

1.4.4 测试分析与测试设计的未来

测试分析与测试设计不仅以前是软件测试工作的基础与重点,未来也仍将是软件测试工作的基础与重点,只不过随着软件开发技术的发展,以及软件技术架构与业务形态的更迭,测试分析与测试设计也会随之发展,最主要的发展方向有以下两个。

(1)以最终用户的价值为目标进行测试分析与测试设计。现在的软件系统都是由特定的人员分析、设计和开发出来的,比如PO和BA人员对软件进行分析并提出需求,UX人员对软件的界面进行设计,接下来Dev人员对软件的技术架构进行设计,最终由Dev和QA人员完成开发和测试工作并生成软件。但很少有软件是由真正懂得使用价值的最终用户参与设计和测试的,而更多是由专业的设计人员进行设计。随着软件的专业程度越来越高,需要软件化的行业也越来越多,我们需要让真正的用户参与业务分析和设计,以及测试分析与测试设计的工作。如果不让用户参与,不以最终用户的价值为目标,就很容易研发出不易使用甚至无用的软件功能,从而导致浪费、返工,事倍功半。

(2)以AI技术为基础,通过AI进行测试分析与测试设计。AI技术发展日新月异,未来将逐步在软件研发和测试领域占据一席之地,它现在已经在代码编写和自动化测试领域崭露头角。但是对于AI技术来讲,最难的还是像人那样理解业务流程,所以用AI技术来实施测试分析与测试设计,仍是一个难题。虽然不能使用AI技术独自实施测试分析与测试设计,但使用AI技术辅助实施测试分析与测试设计还是有可能的,只不过需要有特定的、统一的领域特定语言(Domain Specific Language,DSL)。如果所有的业务流程和验收标准等都可以使用统一的领域特定语言,并且它们都没有歧义,就可以尝试使用深度学习技术来独自实施测试分析与测试设计。不过,学习模型的建立仍有很长的一段路要走。

相关图书

JUnit实战(第3版)
JUnit实战(第3版)
渗透测试技术
渗透测试技术
深入理解软件性能——一种动态视角
深入理解软件性能——一种动态视角
云原生测试实战
云原生测试实战
Android自动化测试实战:Python+Appium +unittest
Android自动化测试实战:Python+Appium +unittest
金融软件测试从入门到实践
金融软件测试从入门到实践

相关文章

相关课程