25-软件测试52讲

开篇词 | 从“小工”到“专家”，我的软件测试修炼之道

你好，我是茹炳晟。我从 2002 年开始做软件开发，2005 年时转型成为测试工程师，算起来已经在这个行业摸爬滚打了 16 年。2005 年的时候，软件测试还停留在重复性手工测试的阶段，而且受重视程度远不如软件开发，所以当时我就像是一个“小工”。

可是，我很快就意识到，要真正把测试这件事做透做精，并不是那么容易，这中间有太多技术需要去研究和探索。

很多人第一印象会觉得做测试比做开发简单很多，但是我想说，在这个世界上，你想把任何一件事做好、做到极致都没那么容易，都需要付出比别人更多的努力。不管是一万小时定律还是厚积薄发，当你把知识积累到一定程度的时候就会发现，原来软件测试的世界这么有意思。

我是国内最早一批从事测试自动化的工程师，并经历了软件测试技术从“原始社会”向“现代文明”发展的整个历程，也经历了从“测试不受重视”到“测试和开发同等重要”的行业理念转变。目前我正在探索由 Google 等一线互联网巨头主导的“去 QE，开发自己测试”的全新模式，也有了很多的感悟和思考。

在这期间，我经历了自动化测试用例设计与开发、测试框架选型、测试框架自行研发、测试基础架构设计以及最新的测试服务化（Test as a Service，TaaS）等一系列技术的变革与发展。

我带领过的测试项目也几乎涵盖了所有种类，包括嵌入式系统测试、金融平台单元测试、平台 SDK 测试、轨道交通安全软件测试、Web Service 测试、大型电商网站 GUI 自动化以及性能全链路压测等。

由此，我个人也完成了从“小工”到“专家”的蜕变，成为了一名资深的测试架构师。

之所以要写这个专栏，我也是希望能把这么多年积累的经验和教训，通过极客时间这个平台，分享给你，让你能够用最短的时间了解整个软件测试行业技术发展的脉络，以“知其然知其所以然”的方式深入理解目前主流的测试技术，不仅知道应该怎么做，更明白为什么要这么做。

同时，在循序渐进的专栏学习过程中，我希望你能够学以致用，将所学应用到你所在企业的实际项目中，帮助企业提高测试质量以及测试效率，同时也为自己的职场晋升之路打好基础。

面对势不可挡的 ABC（AI + Big Data + Cloud）技术浪潮，无论是从被测对象本身的复杂性、多样性和规模性来讲，还是从测试技术以及测试基础架构从无到有的发展来讲，都需要测试工程师的知识面、测试设计能力、测试开发能力和测试平台化抽象能力有质的提升。对此，我结合趋势总结了以下三点。

第一，自动化测试在软件质量工程中的地位发生了质的变化，从原本的“以自动化测试为辅”变成了“以自动化测试为主”。

所以，你不仅需要从业务本身出发来对软件进行手工测试验证，还需要掌握完整的自动化测试开发技术来设计自动化测试用例。

第二，传统软件企业的产品发布通常以“月”为单位，因此，测试执行总时间不会成为关键问题。但是，对于互联网企业，尤其是大型电商网站，产品上线周期通常都是以“天”甚至是以“小时”为单位，留给测试的时间非常有限，这也就对测试执行总时间提出了极大的挑战。为了解决这个难题，你需要一套完善的高并发测试执行基础架构的支持。

所以，作为测试工程师，你就必须掌握设计开发测试基础架构的关键技术。

第三，随着自动化测试的规模化，测试数据准备的各种问题被逐渐暴露并不断放大，成为影响自动化测试效率以及稳定性的“拦路虎”。早期的传统测试数据准备方法，无论是从测试数据准备的时间成本，还是从测试数据的稳定性和测试数据创建的便利性上看，都已经很难适应大规模自动化测试的要求。

所以，你必须系统性地思考如何才能将测试数据的准备工具化，服务化，最终实现平台化。

通过这个专栏的学习，我希望你能够对这些趋势与挑战应对自如，并能时刻紧跟测试技术发展的新趋势。

理想是美好的，但道路往往是曲折的，因为你和我都非常清楚，技术人想要“一口吃成胖子”几乎是不可能的。但是，不要气馁，我在这个专栏里根据多年的从业经验，给你总结了下面这个“三步走”的策略，助你破茧成蝶。

第一步，成为互联网时代合格的测试工程师。

如果你是入行不满 3 年的测试工程师，一定对此有迫切需求。此时，你必须具有快速学习的能力，能迅速掌握被测软件的业务功能与内部架构，并在此基础上运用各种测试方法，尽可能多地发现潜在缺陷，并能够在已知缺陷的基础上进一步发现相关的连带缺陷。

从知识体系上看，你需要有比开发人员更全面的计算机基础知识，还需要了解互联网的基础架构、安全攻击、软件性能、用户体验和常见缺陷等知识。从测试技术上看，你需要能够使用常见的测试框架或者工具，需要具有一定的自动化测试脚本的开发能力，这可以把你从大量重复的工作中解放出来，然后你才能有时间去做更有意思的工作。

第二步，成为互联网时代优秀的测试工程师。

如果你想从“合格”变为“优秀”，那必须先认识到两者的差距在哪里。

首先，合格的测试工程师关注的是纯粹的测试，而优秀的测试工程师关注更多的是软件整体的质量，需要根据业务风险以及影响来制定测试策略，有效控制测试的时间和成本，并且能够对测试框架以及工具做出适合项目需求的选型。

以新房装修为例，合格的测试工程师就是各个工序的装修师傅，他们只管按照设计要求做好自己的工序，而优秀的测试工程师更像是个包工头，他们关心的是整体交付的质量。

其次，优秀的测试工程师不仅可以娴熟地运用各类测试工具，还非常清楚这些测试工具背后的实现原理，以及多个同类测试工具各自的优缺点和适用场景。

在遇到问题时，你还需要能够通过二次开发解决工具和框架层面的问题，对于没有合适可用工具的场景，可以自行设计开发一些小工具来更好地展开测试工作。

当然这个阶段，你很有可能会接触到一些代码级的测试，这就要求你具有一定的开发背景，并能够很好地理解代码级的测试技术。

最后，随着自动化测试用例的不断增长，自动化测试的关注点也从原本的“如何把手工测试步骤用自动化脚本实现”变成了“如何构建低维护成本，可以灵活组装的自动化脚本”，这就要求你理解自动化脚本的分层设计、页面对象模型以及业务流程模型，并且能够把这些设计应用到你的测试框架里。

第三步，成为互联网时代的测试架构师。

当你经历了各种类型的测试项目，就会发现这些项目本身虽然差异巨大，但是有很多东西是相通的。

比如，面对大量测试用例的执行，无论是 GUI 还是 API，都需要一套高效的能够支持高并发的测试执行基础架构；再比如，面对测试过程中的大量差异性数据要求，需要统一的测试数据准备平台；再比如，为了可以更方便地和持续集成与发布系统（CI/CD）以解耦的形式做集成，需要统一发起测试执行的接口。

这样的例子还有很多，如果你已经能够站在这样的高度看待软件测试，那么恭喜你，你已经具备了测试架构师的视野。当然，你还必须对一些前沿的测试方法和技术有自己的理解，并能够在恰当的时候、因地制宜地把它们应用到实际项目中。

这就是我给你总结的“三步走”进阶策略了。千里之行始于足下，接下来我会从测试基础知识讲起，结合实际案例，由浅入深地带你温故知新，提升自己的软件测试技能。

未来的四个月，我将和你一起探讨交流测试世界里各种有意思的技术，也希望四个月后，你我都能遇见更好的自己。

01 | 你真的懂测试吗？从“用户登录”测试谈起

作为专栏的第一篇文章，我选择了一个你耳熟能详的“用户登录”功能作为测试对象，希望通过这样一个简单直白的功能帮助你理解如何做好测试，以及现阶段你需要加强和提高的测试技能。

可能你会说，“用户登录”这个测试对象也有点太简单了吧，我只要找一个用户，让他在界面上输入用户名和密码，然后点击“确认”按钮，验证一下是否登录成功就可以了。的确，这构成了一个最基本、最典型的测试用例，这也是终端用户在使用系统时最典型的 Happy Path 场景。

但是作为测试工程师，你的目标是要保证系统在各种应用场景下的功能是符合设计要求的，所以你需要考虑的测试用例就需要更多、更全面，于是你可能会根据“用户登录”功能的需求描述，结合等价类划分和边界值分析方法来设计一系列的测试用例。

那什么是等价类划分和边界值分析方法呢？首先，这二者都隶属于最常用、最典型、也是最重要的黑盒测试方法。

等价类划分方法，是将所有可能的输入数据划分成若干个子集，在每个子集中，如果任意一个输入数据对于揭露程序中潜在错误都具有同等效果，那么这样的子集就构成了一个等价类。后续只要从每个等价类中任意选取一个值进行测试，就可以用少量具有代表性的测试输入取得较好的测试覆盖结果。

边界值分析方法，是选取输入、输出的边界值进行测试。因为通常大量的软件错误是发生在输入或输出范围的边界上，所以需要对边界值进行重点测试，通常选取正好等于、刚刚大于或刚刚小于边界的值作为测试数据。

从方法论上可以看出来，边界值分析是对等价类划分的补充，所以这两种测试方法经常结合起来使用。

现在，针对“用户登录”功能，基于等价类划分和边界值分析方法，我们设计的测试用例包括：

输入已注册的用户名和正确的密码，验证是否登录成功；

输入已注册的用户名和不正确的密码，验证是否登录失败，并且提示信息正确；

输入未注册的用户名和任意密码，验证是否登录失败，并且提示信息正确；

用户名和密码两者都为空，验证是否登录失败，并且提示信息正确；

用户名和密码两者之一为空，验证是否登录失败，并且提示信息正确；

如果登录功能启用了验证码功能，在用户名和密码正确的前提下，输入正确的验证码，验证是否登录成功；

如果登录功能启用了验证码功能，在用户名和密码正确的前提下，输入错误的验证码，验证是否登录失败，并且提示信息正确。

列出这些测试用例后，你可能已经觉得比较满意了，因为你感觉已经把自己的测试知识都用在这些用例设计中了。

的确，上面的测试用例集已经涵盖了主要的功能测试场景。但是在一个优秀的测试工程师眼中，这些用例只能达到勉强及格的标准。

什么？才刚刚及格？如果你有这个想法，那我建议你在继续看下面的内容前，先仔细思考一下，这些测试用例是否真的还需要扩充。

现在，我跟你分享一下有经验的测试工程师会再增加的测试用例：

用户名和密码是否大小写敏感；

页面上的密码框是否加密显示；

后台系统创建的用户第一次登录成功时，是否提示修改密码；

忘记用户名和忘记密码的功能是否可用；

前端页面是否根据设计要求限制用户名和密码长度；

如果登录功能需要验证码，点击验证码图片是否可以更换验证码，更换后的验证码是否可用；

刷新页面是否会刷新验证码；

如果验证码具有时效性，需要分别验证时效内和时效外验证码的有效性；

用户登录成功但是会话超时后，继续操作是否会重定向到用户登录界面；

不同级别的用户，比如管理员用户和普通用户，登录系统后的权限是否正确；

页面默认焦点是否定位在用户名的输入框中；

快捷键 Tab 和 Enter 等，是否可以正常使用。

看完这些用例，你可能会说：“哇塞，原来一个简简单单的登录功能居然有这么多需要测试的点”。但是，你别高兴得太早，“用户登录”功能的测试还没结束。

虽然改进后的测试用例集相比之前的测试覆盖率的确已经提高了很多，但是站在资深测试人员的角度来看，还有很多用例需要设计。

经我这么一说，你可能已经发现，上面所有的测试用例设计都是围绕显式功能性需求的验证展开的，换句话说，这些用例都是直接针对“用户登录”功能的功能性进行验证和测试的。

但是，一个质量过硬的软件系统，除了显式功能性需求以外，其他的非功能性需求即隐式功能性需求也是极其关键的。

显式功能性需求（Functional requirement）的含义从字面上就可以很好地理解，指的是软件本身需要实现的具体功能， 比如“正常用户使用正确的用户名和密码可以成功登录”、“非注册用户无法登录”等，这都是属于典型的显式功能性需求描述。

那什么是非功能性需求（Non-functional requirement）呢？从软件测试的维度来看，非功能性需求主要涉及安全性、性能以及兼容性三大方面。 在上面所有的测试用例设计中，我们完全没有考虑对非功能性需求的测试，但这些往往是决定软件质量的关键因素。

明白了非功能性需求测试的重要性后，你可以先思考一下还需要设计哪些测试用例，然后再来看看我会给出哪些用例，相信这种方式对你的帮助会更大。

安全性测试用例包括：

用户密码后台存储是否加密；

用户密码在网络传输过程中是否加密；

密码是否具有有效期，密码有效期到期后，是否提示需要修改密码；

不登录的情况下，在浏览器中直接输入登录后的 URL 地址，验证是否会重新定向到用户登录界面；

密码输入框是否不支持复制和粘贴；

密码输入框内输入的密码是否都可以在页面源码模式下被查看；

用户名和密码的输入框中分别输入典型的“SQL 注入攻击”字符串，验证系统的返回页面；

用户名和密码的输入框中分别输入典型的“XSS 跨站脚本攻击”字符串，验证系统行为是否被篡改；

连续多次登录失败情况下，系统是否会阻止后续的尝试以应对暴力破解；

同一用户在同一终端的多种浏览器上登录，验证登录功能的互斥性是否符合设计预期；

同一用户先后在多台终端的浏览器上登录，验证登录是否具有互斥性。

性能压力测试用例包括:

单用户登录的响应时间是否小于 3 秒；

单用户登录时，后台请求数量是否过多；

高并发场景下用户登录的响应时间是否小于 5 秒；

高并发场景下服务端的监控指标是否符合预期；

高集合点并发场景下，是否存在资源死锁和不合理的资源等待；

长时间大量用户连续登录和登出，服务器端是否存在内存泄漏。

兼容性测试用例包括：

不同浏览器下，验证登录页面的显示以及功能正确性；

相同浏览器的不同版本下，验证登录页面的显示以及功能正确性；

不同移动设备终端的不同浏览器下，验证登录页面的显示以及功能正确性；

不同分辨率的界面下，验证登录页面的显示以及功能正确性。

说到这里，你还会觉得“用户登录”功能的测试非常简单、不值一提么？一个看似简单的功能测试，居然涵盖了如此多的测试用例，除了要覆盖明确的功能性需求，还需要考虑其他诸多的非功能性需求。

另外，通过这些测试用例的设计，你也可以发现，一个优秀的测试工程师必须具有很宽广的知识面，如果你不能对被测系统的设计有深入的理解、不明白安全攻击的基本原理、没有掌握性能测试的基本设计方法，很难设计出“有的放矢”的测试用例。

通过“用户登录”功能测试这个实例，我希望可以激发你对测试更多的思考，并且开拓你设计测试用例的思路，以达到抛砖引玉的效果。

看完了这些测试用例，你可能会说还有一些遗漏的测试点没有覆盖到，这个功能的测试点还不够全面。那么，接下来我再跟你说说测试的不可穷尽性，即绝大多数情况下，是不可能进行穷尽测试的。

所谓的“穷尽测试”是指包含了软件输入值和前提条件所有可能组合的测试方法，完成穷尽测试的系统里应该不残留任何未知的软件缺陷。 因为如果有未知的软件缺陷，你可以通过做更多的测试来找到它们，也就是说你的测试还没有穷尽。

但是，在绝大多数的软件工程实践中，测试由于受限于时间成本和经济成本，是不可能去穷尽所有可能的组合的，而是采用基于风险驱动的模式，有所侧重地选择测试范围和设计测试用例，以寻求缺陷风险和研发成本之间的平衡。

总结

首先，对于高质量的软件测试，用例设计不仅需要考虑明确的显式功能性需求，还要涉及兼容性、安全性和性能等一系列的非功能性需求，这些非功能性需求对软件系统的质量有着举足轻重的作用。

其次，优秀的测试工程师必须具有宽广的知识面，才能设计出有针对性、更易于发现问题的测试用例。

最后，软件测试的用例设计是不可穷尽的，工程实践中难免受制于时间成本和经济成本，所以优秀的测试工程师需要兼顾缺陷风险和研发成本之间的平衡。

思考题

从拓展思维的角度，请你思考一下“用户登录”功能是否还可以添加更多的测试用例。基于同样的思路，思考一下你目前工作中的测试用例设计是否需要加入更多的测试点。

欢迎你给我留言。

02 | 如何设计一个“好的”测试用例？

在上一篇文章中，我以“用户登录”这一简单直接的功能作为测试对象，为你介绍了如何设计测试用例。现在你应该已经知道，为了保证软件系统的质量，测试用例的设计不仅需要考虑功能性需求，还要考虑大量的非功能性需求。

那么，今天我会重点和你探讨如何才能设计出一个“好的”测试用例。

什么才算是“好的”测试用例？

在正式开始讨论之前，我先跟你聊聊，什么才是“好的”测试用例，这个“好”又应该体现在哪些方面。这是一个看似简单实则难以回答的问题，即使深入思考后，也很难有非常标准的答案。

通常，你的第一反应很可能会是“发现了软件缺陷的测试用例就是好的用例”，我可能会反问你“如果说测试用例发现了缺陷就是好用例，那么在该缺陷被修复后，同样的用例难道就不是好用例了吗？”。

你可能还会说“发现软件缺陷可能性大的测试用例就是好用例”，这话看起来还是蛮有道理的，但是我同样会反问你“你打算用什么方法来量化测试用例发现缺陷的可能性？”。

类似地，你可能还会说“发现至今未被发现的软件缺陷的测试用例就是好用例”，那么我想问你的是：如何评估是否还存在未被发现的缺陷？如果软件中根本就没有错误了呢？

其实，是你定义“好的”测试用例的思路错了，这就有点像“傻子吃烧饼”，连吃五个不饱，吃完第六个终于饱了，于是他说：早知道吃了第六个就会饱，何必吃前面五个呢。细想，他吃的六个烧饼其实是一个整体，一起吃下去才会饱，而你无法找到吃一个就能饱的“好”烧饼。

对于测试用例其实也是同样的道理，“好的”测试用例一定是一个完备的集合，它能够覆盖所有等价类以及各种边界值，而跟能否发现缺陷无关。

我举一个“池塘捕鱼”的例子，可以帮你更好地理解什么是“好的”测试用例。

如果把被测试软件看作一个池塘，软件缺陷是池塘中的鱼，建立测试用例集的过程就像是在编织一张捕渔网。“好的”测试用例集就是一张能够覆盖整个池塘的大渔网，只要池塘里有鱼，这个大渔网就一定能把鱼给捞上来。

如果渔网本身是完整的且合格的，那么捞不到鱼，就证明池塘中没有鱼，而渔网的好坏与池塘中是否有鱼无关。

“好的”测试用例必须具备哪些特征？

一个“好的”测试用例，必须具备以下三个特征。

整体完备性： “好的”测试用例一定是一个完备的整体，是有效测试用例组成的集合，能够完全覆盖测试需求。

等价类划分的准确性： 指的是对于每个等价类都能保证只要其中一个输入测试通过，其他输入也一定测试通过。

等价类集合的完备性： 需要保证所有可能的边界值和边界条件都已经正确识别。

做到了以上三点，就可以肯定测试是充分且完备的，即做到了完整的测试需求覆盖。

三种最常用的测试用例设计方法

明白了“好的”测试用例的内涵和外延后，我再回过头来给你讲讲，为了能够设计出“好的”测试用例，你通常都要使用哪些设计方法。

从理论层面来讲，设计用例的方法有很多，如果你去翻阅测试图书或网络教程，会发现一堆让人眼花缭乱的测试方法，比如等价类划分法、边界值分析法、错误推测方法、因果图方法、判定表驱动分析法、正交实验设计方法、功能图分析方法、场景设计方法、形式化方法、扩展有限状态机方法等等，但是从软件企业实际的工程实践来讲，真正具有实用价值并且常用的只有前三种方法。

当然，对于那些与人的生命安全直接或间接相关的软件，比如飞行控制、轨道交通的列车控制、医疗检测相关的软件或者系统，由于需要达到几近变态的测试覆盖率要求，会采用更多的测试设计方法。但对大多数的软件测试而言，综合使用等价类划分、边界值分析和错误推测这三大类方法就足够了。

接下来，我会结合实际的例子，给你解释一下这三类方法的核心概念以及在使用时需要注意的问题。

第一，等价类划分方法

从上一篇文章中你已经知道了，等价类中任意一个输入数据对于揭露程序中潜在错误都具有同等效果。后续我们只要从每个等价类中任意选取一个值进行测试，就可以用少量具有代表性的测试输入取得较好的测试覆盖结果。

现在，我给你看一个具体的例子：学生信息系统中有一个“考试成绩”的输入项，成绩的取值范围是 0~100 之间的整数，考试成绩及格的分数线是 60。

为了测试这个输入项，显然不可能用 0~100 的每一个数去测试。通过需求描述可以知道，输入 0~59 之间的任意整数，以及输入 60~100 之间的任意整数，去验证和揭露输入框的潜在缺陷可以看做是等价的。

那么这就可以在 0~59 和 60~100 之间各随机抽取一个整数来进行验证。这样的设计就构成了所谓的“有效等价类”。

你不要觉得进行到这里，已经完成了等价类划分的工作，因为等价类划分方法的另一个关键点是要找出所有“无效等价类”。显然，如果输入的成绩是负数，或者是大于 100 的数等都构成了“无效等价类”。

在考虑了无效等价类后，最终设计的测试用例为：

有效等价类 1：0~59 之间的任意整数；

有效等价类 2：59~100 之间的任意整数；

无效等价类 1：小于 0 的负数；

无效等价类 2：大于 100 的整数；

无效等价类 3：0~100 之间的任何浮点数；

无效等价类 4：其他任意非数字字符。

第二，边界值分析方法

边界值分析是对等价类划分的补充，你从工程实践经验中可以发现，大量的错误发生在输入输出的边界值上，所以需要对边界值进行重点测试，通常选取正好等于、刚刚大于或刚刚小于边界的值作为测试数据。

我们继续看学生信息系统中“考试成绩”的例子，选取的边界值数据应该包括：-1，0，1，59，60，61，99，100，101。

第三，错误推测方法

错误推测方法是指基于对被测试软件系统设计的理解、过往经验以及个人直觉，推测出软件可能存在的缺陷，从而有针对性地设计测试用例的方法。这个方法强调的是对被测试软件的需求理解以及设计实现的细节把握，当然还有个人的能力。

错误推测法和目前非常流行的“探索式测试方法”的基本思想和理念是不谋而合的，这类方法在目前的敏捷开发模式下的投入产出比很高，因此被广泛应用。但是，这个方法的缺点也显而易见，那就是难以系统化，并且过度依赖个人能力。

比如，Web 界面的 GUI 功能测试，需要考虑浏览器在有缓存和没有缓存下的表现；Web Service 的 API 测试，需要考虑被测 API 所依赖的第三方 API 出错下的处理逻辑；对于代码级的单元测试，需要考虑被测函数的输入参数为空情况下的内部处理逻辑等等。由此可见，这些测试用例的设计都是基于曾经遇到的问题而进行的错误推测，很大程度上取决于个人能力。

在软件企业的具体实践中，为了降低对个人能力的依赖，通常会建立常见缺陷知识库，在测试设计的过程中，会使用缺陷知识库作为检查点列表（checklist），去帮助优化补充测试用例的设计。

对于中小企业，可能最初的方法就是建立一个简单的 wiki 页面，让测试工程师完成测试用例的最初设计后对应这个 wiki 页面先做一轮自检，如果在后续测试中发现了新的点，就会继续完善这个 wiki 页面。

对于测试基础架构比较成熟的中大型软件企业，通常会以该缺陷知识库作为数据驱动测试的输入来自动生成部分的测试数据，这部分内容我会在后面的文章中详细介绍。

如何才能设计出“好的”测试用例？

掌握了最基本的三种设计测试用例的方法，你就相当于拿到了打仗所需要的枪支弹药，接下来就是如何在实战中用这些武器打个大胜仗了。

在真实的工程实践中，不同的软件项目在研发生命周期的各个阶段都会有不同的测试类型。 比如，传统软件的开发阶段通常会有单元测试，软件模块集成阶段会有代码级集成测试，打包部署后会有面向终端用户的 GUI 测试；再比如，电商网站的测试会分为服务器端基于 API 的测试、中间件测试、前端 GUI 测试等。

对于每一种不同的测试类型，设计出“好的”测试用例的关注点和方法论可能会有很大的差异， 有些可能采用黑盒方法，有些可能采用白盒方法，有些还会采用灰盒方法（比如，微服务架构中的测试），所以很难有一套放之四海而皆准的套路。

所以，在这篇文章中，我仅以最常见、最容易理解的面向终端用户的 GUI 测试为例，跟你聊聊如何才能设计一个“好的”测试用例。

面向终端用户的 GUI 测试，最核心的测试点就是验证软件对需求的满足程度，这就要求测试工程师对被测软件的需求有深入的理解。在我看来，深入理解被测软件需求的最好方法是，测试工程师在需求分析和设计阶段就开始介入，因为这个阶段是理解和掌握软件的原始业务需求的最好时机。

只有真正理解了原始业务需求之后，才有可能从业务需求的角度去设计针对性明确、从终端用户使用场景考虑的端到端（End-2-End）的测试用例集。这个阶段的测试用例设计，主要目的是验证各个业务需求是否被满足，主要采用基于黑盒的测试设计方法。

在具体的用例设计时，首先需要搞清楚每一个业务需求所对应的多个软件功能需求点，然后分析出每个软件功能需求点对应的多个测试需求点，最后再针对每个测试需求点设计测试用例。

这个用例设计过程，你可能觉得有点绕，但是没关系，我以“用户登录”功能的测试用例设计为例，画了一张图来帮你理清这些概念之间的映射关系。

图中的业务需求到软件功能需求、软件功能需求到测试需求，以及测试需求到测试用例的映射关系，在非互联网软件企业的实践中，通常会使用需求追踪管理工具（比如 ALM、DOORS、JIRA、TestLink 等）来管理，并以此来衡量测试用例对业务需求、软件功能需求的覆盖率。

具体到测试用例本身的设计，有两个关键点需要你注意。

从软件功能需求出发，全面地、无遗漏地识别出测试需求是至关重要的，这将直接关系到用例的测试覆盖率。 比如，如果你没有识别出用户登录功能的安全性测试需求，那么后续设计的测试用例就完全不会涉及安全性，最终造成重要测试漏洞。

对于识别出的每个测试需求点，需要综合运用等价类划分、边界值分析和错误推测方法来全面地设计测试用例。 这里需要注意的是，要综合运用这三种方法，并针对每个测试需求点的具体情况，进行灵活选择。
以“用户登录”的功能性测试需求为例，你首先应该对“用户名”和“密码”这两个输入项分别进行等价类划分，列出对应的有效等价类和无效等价类，对于无效等价类的识别可以采用错误猜测法（比如，用户名包含特殊字符等），然后基于两者可能的组合，设计出第一批测试用例。
等价类划分完后，你需要补充“用户名”和“密码”这两个输入项的边界值的测试用例，比如用户名为空（NULL）、用户名长度刚刚大于允许长度等。

用例设计的其他经验

除了上面介绍的方法外，我再跟你分享三个独家“秘籍”，希望能够帮你设计出“好的”测试用例集。

只有深入理解被测试软件的架构，你才能设计出“有的放矢”的测试用例集，去发现系统边界以及系统集成上的潜在缺陷。
作为测试工程师，切忌不能把整个被测系统看作一个大黑盒，你必须对内部的架构有清楚的认识，比如数据库连接方式、数据库的读写分离、消息中间件 Kafka 的配置、缓存系统的层级分布、第三方系统的集成等等。

必须深入理解被测软件的设计与实现细节，深入理解软件内部的处理逻辑。
单单根据测试需求点设计的用例，只能覆盖“表面”的一层，往往会覆盖不到内部的处理流程、分支处理，而没有覆盖到的部分就很可能出现缺陷遗漏。在具体实践中，你可以通过代码覆盖率指标找出可能的测试遗漏点。
同时，切忌不要以开发代码的实现为依据设计测试用例。因为开发代码实现的错误会导致测试用例也出错，所以你应该根据原始需求设计测试用例。

需要引入需求覆盖率和代码覆盖率来衡量测试执行的完备性，并以此为依据来找出遗漏的测试点。 关于什么是需求覆盖率和代码覆盖率，我会在后续的文章中详细介绍。

总结

最后，我来简单总结一下今天的主要内容。

首先，你需要明白，“好的”测试用例一定是一个完备的集合，它能够覆盖所有等价类以及各种边界值，而能否发现软件缺陷并不是衡量测试用例好坏的标准。

其次，设计测试用例的方法有很多种，但综合运用等价类划分、边界值分析和错误推测方法，可以满足绝大多数软件测试用例设计的需求。

再次，“好的”测试用例在设计时，需要从软件功能需求出发，全面地、无遗漏地识别出测试需求至关重要。

最后，如果想设计一个“好的”测试用例，你必须要深入理解被测软件的架构设计，深入软件内部的处理逻辑，需求覆盖率和代码覆盖率这两个指标可以帮你衡量测试执行的完备性。

思考题

在设计测试用例的过程中，你有哪些可供分享的好的实践和方法？

欢迎你给我留言。

03 | 什么是单元测试？如何做好单元测试？

今天我要跟你分享的主题是单元测试，如果你没有开发背景，感觉这篇文章理解起来有难度，那你可以在学完后续的“代码级测试”系列的文章后，再回过头来看一遍这篇文章，相信你会有醍醐灌顶的感觉。

什么是单元测试？

在正式开始今天的话题之前，我先给你分享一个工厂生产电视机的例子。

工厂首先会将各种电子元器件按照图纸组装在一起构成各个功能电路板，比如供电板、音视频解码板、射频接收板等，然后再将这些电路板组装起来构成一个完整的电视机。

如果一切顺利，接通电源后，你就可以开始观看电视节目了。但是很不幸，大多数情况下组装完成的电视机根本无法开机，这时你就需要把电视机拆开，然后逐个模块排查问题。

假设你发现是供电板的供电电压不足，那你就要继续逐级排查组成供电板的各个电子元器件，最终你可能发现罪魁祸首是一个电容的故障。这时，为了定位到这个问题，你已经花费了大量的时间和精力。

那在后续的生产中，如何才能避免类似的问题呢？

你可能立即就会想到，为什么不在组装前，就先测试每个要用到的电子元器件呢？这样你就可以先排除有问题的元器件，最大程度地防止组装完成后逐级排查问题的事情发生。

实践也证明，这的确是一个行之有效的好办法。

如果把电视机的生产、测试和软件的开发、测试进行类比，你可以发现：

电子元器件就像是软件中的单元，通常是函数或者类，对单个元器件的测试就像是软件测试中的单元测试；

组装完成的功能电路板就像是软件中的模块，对电路板的测试就像是软件中的集成测试；

电视机全部组装完成就像是软件完成了预发布版本，电视机全部组装完成后的开机测试就像是软件中的系统测试。

通过这个类比，相信你已经体会到了单元测试对于软件整体质量的重要性，那么单元测试到底是什么呢？

单元测试是指，对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作，这里的最小可测试单元通常是指函数或者类。

单元测试通常由开发工程师完成，一般会伴随开发代码一起递交至代码库。单元测试属于最严格的软件测试手段，是最接近代码底层实现的验证手段，可以在软件开发的早期以最小的成本保证局部代码的质量。

另外，单元测试都是以自动化的方式执行，所以在大量回归测试的场景下更能带来高收益。

同时，你还会发现，单元测试的实施过程还可以帮助开发工程师改善代码的设计与实现，并能在单元测试代码里提供函数的使用示例，因为单元测试的具体表现形式就是对函数以各种不同输入参数组合进行调用，这些调用方法构成了函数的使用说明。

如何做好单元测试？

要做好单元测试，你首先必须弄清楚单元测试的对象是代码，以及代码的基本特征和产生错误的原因，然后你必须掌握单元测试的基本方法和主要技术手段，比如什么是驱动代码、桩代码和 Mock 代码等。

第一，代码的基本特征与产生错误的原因

开发语言多种多样，程序实现的功能更是千变万化，我可以提炼出代码的基本特征，并总结出代码缺陷的主要原因么？答案是肯定，你静下心来思考时，会发现其中是有规律可寻的。

因为无论是开发语言还是脚本语言，都会有条件分支、循环处理和函数调用等最基本的逻辑控制，如果抛开代码需要实现的具体业务逻辑，仅看代码结构的话，你会发现所有的代码都是在对数据进行分类处理，每一次条件判定都是一次分类处理，嵌套的条件判定或者循环执行，也是在做分类处理。

如果有任何一个分类遗漏，都会产生缺陷；如果有任何一个分类错误，也会产生缺陷；如果分类正确也没有遗漏，但是分类时的处理逻辑错误，也同样会产生缺陷。

可见，要做到代码功能逻辑正确，必须做到分类正确并且完备无遗漏，同时每个分类的处理逻辑必须正确。

在具体的工程实践中，开发工程师为了设计并实现逻辑功能正确的代码，通常会有如下的考虑过程：

如果要实现正确的功能逻辑，会有哪几种正常的输入；

是否有需要特殊处理的多种边界输入；

各种潜在非法输入的可能性以及如何处理。

讲到这里，你有没有回想起我跟你分享的“等价类”。没错，这些开发工程师眼中的代码“功能点”，就是单元测试的“等价类”。

第二，单元测试用例详解

在实际工作中，你想做好单元测试，就必须对单元测试的用例设计有深入的理解。

通常来讲，单元测试的用例是一个“输入数据”和“预计输出”的集合。 你需要针对确定的输入，根据逻辑功能推算出预期正确的输出，并且以执行被测试代码的方式进行验证，用一句话概括就是“在明确了代码需要实现的逻辑功能的基础上，什么输入，应该产生什么输出”。

但是，对于单元测试来讲，测试用例的“输入数据”和“预计输出”可能远比你想得要复杂得多。

首先，让我来解释一下单元测试用例“输入数据”都有哪些种类，如果你想当然的认为只有被测试函数的输入参数是“输入数据”的话，那就大错特错了。 这里我总结了几种“输入数据”，希望可以帮助你理解什么才是完整的单元测试“输入数据”：

被测试函数的输入参数；

被测试函数内部需要读取的全局静态变量；

被测试函数内部需要读取的成员变量；

函数内部调用子函数获得的数据；

函数内部调用子函数改写的数据；

嵌入式系统中，在中断调用时改写的数据；

…

然后，让我们再来看看“预计输出”，如果没有明确的预计输出，那么测试本身就失去了意义。同样地，“预计输出” 绝对不是只有函数返回值这么简单，还应该包括函数执行完成后所改写的所有数据。 具体来看有以下几大类：

被测试函数的返回值；

被测试函数的输出参数；

被测试函数所改写的成员变量；

被测试函数所改写的全局变量；

被测试函数中进行的文件更新；

被测试函数中进行的数据库更新；

被测试函数中进行的消息队列更新；

…

另外，对于预计输出值，你必须严格根据代码的功能逻辑来设定，而不能通过阅读代码来推算预期输出，否则就是“掩耳盗铃”了。

你不要觉得好笑，这种情况经常出现。主要原因是，开发工程师自己测试自己写的代码时会有严重的思维惯性，以至于会根据自己的代码实现来推算预计输出。

最后，我还要再提一个点，如果某些等价类或者边界值，开发工程师在开发的时候都没有考虑到，测试的时候就更不会去设计对应的测试用例了，这样也就会造成测试盲区。

第三，驱动代码，桩代码和 Mock 代码

驱动代码，桩代码和 Mock 代码，是单元测试中最常出现的三个名词。驱动代码是用来调用被测函数的，而桩代码和 Mock 代码是用来代替被测函数调用的真实代码的。

驱动代码，桩代码和 Mock 代码三者的逻辑关系

驱动代码（Driver）指调用被测函数的代码，在单元测试过程中，驱动模块通常包括调用被测函数前的数据准备、调用被测函数以及验证相关结果三个步骤。驱动代码的结构，通常由单元测试的框架决定。

桩代码（Stub）是用来代替真实代码的临时代码。 比如，某个函数 A 的内部实现中调用了一个尚未实现的函数 B，为了对函数 A 的逻辑进行测试，那么就需要模拟一个函数 B，这个模拟的函数 B 的实现就是所谓的桩代码。

为了帮你理解，我带你看下这个例子：假定函数 A 是被测函数，其内部调用了函数 B（具体伪代码如下）：

被测函数 A 内部调用了函数 B

在单元测试阶段，由于函数 B 尚未实现，但是为了不影响对函数 A 自身实现逻辑的测试，你可以用一个假的函数 B 来代替真实的函数 B，那么这个假的函数 B 就是桩函数。

为了实现函数 A 的全路径覆盖，你需要控制不同的测试用例中函数 B 的返回值，那么桩函数 B 的伪代码就应该是这个样子的：

当执行第一个测试用例的时候，桩函数 B 应该返回 true，而当执行第二个测试用例的时候，桩函数 B 应该返回 false。

这样就覆盖了被测试函数 A 的 if-else 的两个分支。

桩函数内部实现

从这个例子可以看出，桩代码的应用首先起到了隔离和补齐的作用，使被测代码能够独立编译、链接，并独立运行。同时，桩代码还具有控制被测函数执行路径的作用。

所以，编写桩代码通常需要遵守以下三个原则：

桩函数要具有与原函数完全相同的原形，仅仅是内部实现不同，这样测试代码才能正确链接到桩函数；

用于实现隔离和补齐的桩函数比较简单，只需保持原函数的声明，加一个空的实现，目的是通过编译链接；

实现控制功能的桩函数是应用最广泛的，要根据测试用例的需要，输出合适的数据作为被测函数的内部输入。

Mock 代码和桩代码非常类似，都是用来代替真实代码的临时代码，起到隔离和补齐的作用。但是很多人，甚至是具有多年单元测试经验的开发工程师，也很难说清这二者的区别。

在我看来，Mock 代码和桩代码的本质区别是：测试期待结果的验证（Assert and Expectiation）。

对于 Mock 代码来说，我们的关注点是 Mock 方法有没有被调用，以什么样的参数被调用，被调用的次数，以及多个 Mock 函数的先后调用顺序。所以，在使用 Mock 代码的测试中，对于结果的验证（也就是 assert），通常出现在 Mock 函数中。

对于桩代码来说，我们的关注点是利用 Stub 来控制被测函数的执行路径，不会去关注 Stub 是否被调用以及怎么样被调用。所以，你在使用 Stub 的测试中，对于结果的验证（也就是 assert），通常出现在驱动代码中。

在这里，我只想让你理解两者的本质区别以确保你知识结构的完整性，如果你想深入比较，可以参考马丁·福勒（Martin Fowler）的著名文章《Mock 代码不是桩代码》（Mocks Aren’t Stubs）。

因为从实际应用的角度看，就算你不能分清 Mock 代码和桩代码，也不会影响你做好单元测试，所以我并没有从理论层面去深入比较它们的区别。

实际项目中如何开展单元测试？

最后我要跟你聊一下，实际软件项目中如何开展单元测试？

并不是所有的代码都要进行单元测试，通常只有底层模块或者核心模块的测试中才会采用单元测试。

你需要确定单元测试框架的选型，这和开发语言直接相关。比如，Java 最常用的单元测试框架是 Junit 和 TestNG；C/C++ 最常用的单元测试框架是 CppTest 和 Parasoft C/C++test；框架选型完成后，你还需要对桩代码框架和 Mock 代码框架选型，选型的主要依据是开发所采用的具体技术栈。
通常，单元测试框架、桩代码 /Mock 代码的选型工作由开发架构师和测试架构师共同决定。

为了能够衡量单元测试的代码覆盖率，通常你还需要引入计算代码覆盖率的工具。不同的语言会有不同的代码覆盖率统计工具，比如 Java 的 JaCoCo，JavaScript 的 Istanbul。在后续的文章中，我还会详细为你介绍代码覆盖率的内容。

最后你需要把单元测试执行、代码覆盖率统计和持续集成流水线做集成，以确保每次代码递交，都会自动触发单元测试，并在单元测试执行过程中自动统计代码覆盖率，最后以“单元测试通过率”和“代码覆盖率”为标准来决定本次代码递交是否能够被接受。

如果你有开发背景，那么入门单元测试是比较容易的。但真正在项目中全面推行单元测试时，你会发现还有一些困难需要克服：

紧密耦合的代码难以隔离；

隔离后编译链接运行困难；

代码本身的可测试性较差，通常代码的可测试性和代码规模成正比；

无法通过桩代码直接模拟系统底层函数的调用；

代码覆盖率越往后越难提高。

总结

我给你详细介绍了单元测试的概念，和你重点讨论了用例的组成，以及在实际项目中开展单元测试的方法，你需要注意以下三个问题：

代码要做到功能逻辑正确，必须做到分类正确并且完备无遗漏，同时每个分类的处理逻辑必须正确；

单元测试是对软件中的最小可测试单元在与软件其他部分相隔离的情况下进行的代码级测试；

桩代码起到了隔离和补齐的作用，使被测代码能够独立编译、链接，并运行。

思考题

你所在的公司有做单元测试吗？实施单元测试过程中遇到过哪些问题，你是如何解决的？

欢迎你给我留言。

04 | 为什么要做自动化测试？什么样的项目适合做自动化测试？

在上一篇文章中，我为你介绍了什么是单元测试，以及如何做好单元测试，今天我来跟你聊聊什么是自动化测试，为什么要做自动化测试，以及什么样的项目适合做自动化测试。

什么是自动化测试？

不管你是刚入行的小白，还是已经在做软件测试的工作，相信你一定听说过或者接触过自动化测试。那么，自动化测试到底是什么意思呢？

顾名思义，自动化测试是，把人对软件的测试行为转化为由机器执行测试行为的一种实践，对于最常见的 GUI 自动化测试来讲，就是由自动化测试工具模拟之前需要人工在软件界面上的各种操作，并且自动验证其结果是否符合预期。

你是不是有点小激动？这似乎开启了用机器代替重复手工劳动的自动化时代，你可以从简单重复劳动中解放出来了。但现实呢？

自动化测试的本质是先写一段代码，然后去测试另一段代码，所以实现自动化测试用例本身属于开发工作，需要投入大量的时间和精力，并且已经开发完成的用例还必须随着被测对象的改变而不断更新，你还需要为此付出维护测试用例的成本。

当你发现自动化测试用例的维护成本高于其节省的测试成本时，自动化测试就失去了价值与意义，你也就需要在是否使用自动化测试上权衡取舍了。

为什么需要自动化测试？

为了让你更好地理解自动化测试的价值，即为什么需要自动化测试，我先来跟你聊聊自动化测试通常有哪些优势：

自动化测试可以替代大量的手工机械重复性操作，测试工程师可以把更多的时间花在更全面的用例设计和新功能的测试上；

自动化测试可以大幅提升回归测试的效率，非常适合敏捷开发过程；

自动化测试可以更好地利用无人值守时间，去更频繁地执行测试，特别适合现在非工作时间执行测试，工作时间分析失败用例的工作模式；

自动化测试可以高效实现某些手工测试无法完成或者代价巨大的测试类型，比如关键业务 7×24 小时持续运行的系统稳定性测试和高并发场景的压力测试等；

自动化测试还可以保证每次测试执行的操作以及验证的一致性和可重复性，避免人为的遗漏或疏忽。

而为了避免对自动化测试的过度依赖，你还需要了解自动化测试有哪些劣势，这将帮你绕过实际工作中的“坑”。

自动化测试并不能取代手工测试，它只能替代手工测试中执行频率高、机械化的重复步骤。你千万不要奢望所有的测试都自动化，否则一定会得不偿失。

自动测试远比手动测试脆弱，无法应对被测系统的变化，业界一直有句玩笑话“开发手一抖，自动化测试忙一宿”，这也从侧面反映了自动化测试用例的维护成本一直居高不下的事实。
其根本原因在于自动化测试本身不具有任何“智能”，只是按部就班地执行事先定义好的测试步骤并验证测试结果。对于执行过程中出现的明显错误和意外事件，自动化测试没有任何处理能力。

自动化测试用例的开发工作量远大于单次的手工测试，所以只有当开发完成的测试用例的有效执行次数大于等于 5 次时，才能收回自动化测试的成本。

手工测试发现的缺陷数量通常比自动化测试要更多，并且自动化测试仅仅能发现回归测试范围的缺陷。

测试的效率很大程度上依赖自动化测试用例的设计以及实现质量，不稳定的自动化测试用例实现比没有自动化更糟糕。

实行自动化测试的初期，用例开发效率通常都很低，大量初期开发的用例通常会在整个自动化测试体系成熟，和测试工程师全面掌握测试工具后，需要重构。

业务测试专家和自动化测试专家通常是两批人，前者懂业务不懂自动化技术，后者懂自动化技术但不懂业务，只有二者紧密合作，才能高效开展自动化测试。

自动化测试开发人员必须具备一定的编程能力，这对传统的手工测试工程师会是一个挑战。

什么样的项目适合自动化测试？

看到这里，你心里可能在暗自嘀咕，“有没有搞错啊，自动化测试的劣势居然比优势还多”。那为什么还有那么多的企业级项目在实行自动化测试呢？那么，我接下来要讲的内容就是，到底什么样的项目适合自动化测试？

第一，需求稳定，不会频繁变更。

自动化测试最怕的就是需求不稳定，过高的需求变更频率会导致自动化测试用例的维护成本直线上升。刚刚开发完成并调试通过的用例可能因为界面变化，或者是业务流程变化，不得不重新开发调试。所以自动化测试更适用于需求相对稳定的软件项目。

第二，研发和维护周期长，需要频繁执行回归测试。

1. 在我看来，软件产品比软件项目更适合做自动化测试。

首先，软件产品的生命周期一般都比较长，通常会有多个版本陆续发布，每次版本发布都会有大量的回归测试需求。

同时，软件产品预留给自动化测试开发的时间也比较充裕，可以和产品一起迭代。

其次，自动化测试用例的执行比高于 1:5，即开发完成的用例至少可以被有效执行 5 次以上时，自动化测试的优势才可以被更好地体现。

2. 对于软件项目的自动化测试，就要看项目的具体情况了。

如果短期的一次性项目，就算从技术上讲自动化测试的可行性很高，但从投入产出比（ROI）的角度看并不建议实施自动化，因为千辛万苦开发完成的自动化用例可能执行一两次，项目就结束了。我还遇到过更夸张的情况，自动化测试用例还没开发完，项目都已经要上线了。

所以，对于这种短期的一次性项目，我觉得你应该选择手工探索式测试，以发现缺陷为第一要务。而对于一些中长期项目，我的建议是：对比较稳定的软件功能进行自动化测试，对变动较大或者需求暂时不明确的功能进行手工测试，最终目标是用 20% 的精力去覆盖 80% 的回归测试。

第三，需要在多种平台上重复运行相同测试的场景。

这样的场景其实有很多，比如：

对于 GUI 测试，同样的测试用例需要在多种不同的浏览器上执行；

对于移动端应用测试，同样的测试用例需要在多个不同的 Android 或者 iOS 版本上执行，或者是同样的测试需要在大量不同的移动终端上执行；

对于一些企业级软件，如果对于不同的客户有不同的定制版本，各个定制版本的主体功能绝大多数是一致的，可能只有个别功能有轻微差别，测试也是需要覆盖每个定制版本的所有测试；

……

这些都是自动化测试的最佳应用场景，因为单个测试用例都需要被反复执行多次，能够使自动化测试的投资回报率最大化。

第四，某些测试项目通过手工测试无法实现，或者手工成本太高。

对于所有的性能和压力测试，很难通过手工方式实现。

比如，某一个项目要求进行一万并发用户的基准性能测试（Benchmark test），难道你真的要找一万个用户按照你的口令来操作被测软件吗？又比如，对于 7×24 小时的稳定性测试，难道你也要找一批用户没日没夜地操作被测软件吗？

这个时候，你就必须借助自动化测试技术了，用机器来模拟大量用户反复操作被测软件的场景。当然对于此类测试是不可能通过 GUI 操作来模拟大量用户行为的，你必须基于协议的自动化测试技术，这个我会在后续的性能测试章节详细叙述。

第五，被测软件的开发较为规范，能够保证系统的可测试性。

从技术上讲，如果要实现稳定的自动化测试，被测软件的开发过程就必须规范。比如，GUI 上的控件命名如果没有任何规则可寻，就会造成 GUI 自动化的控件识别与定位不稳定，从而影响自动化测试的效率。

另外，某些用例的自动化必须要求开发人员在产品中预留可测试性接口，否则后续的自动化会很难开展。

比如，有些用户登录操作，需要图片验证码，如果开发人员没有提供绕开图片验证码的路径，那么自动化测试就必须借助光学字符识别（OCR）技术来对图片验证码进行模式识别，而它的设计初衷是为了防止机器人操作，可想而知 OCR 的识别率会很低，就会直接影响用例的稳定性。

第六，测试人员已经具备一定的编程能力。

如果测试团队的成员没有任何开发编程的基础，那你想要推行自动化测试就会有比较大的阻力。这个阻力会来自于两个方面：

前期的学习成本通常会比较大，很难在短期内对实际项目产生实质性的帮助，此时如果管理层对自动化测试没有正确的预期，很可能会叫停自动化测试；

测试工程师通常会非常热衷于学习使用自动化测试技术，以至于他们的工作重点会发生错误的偏移，把大量的精力放在自动化测试技术的学习与实践上，而忽略了测试用例的设计，这将直接降低软件整体的质量。

总结

自动化测试是，把人工对软件的测试转化为由机器执行测试行为的一种实践，可以把测试工程师从机械重复的测试工作中解脱出来，将更多的精力放在新功能的测试和更全面的测试用例设计上。

然而自动化测试试一把“双刃剑”，虽然它可以从一定程度上解放测试工程师的劳动力，完成一些人工无法实现的测试，但并不适用于所有的测试场景，如果维护自动化测试的代价高过了节省的测试成本，那么在这样的项目中推进自动化测试就会得不偿失。

思考题

你在实际项目中接触过哪些自动化测试，自动化测试用例的执行比通常是多少？如果执行比过低，需要频繁更新测试用例，那你思考过你的项目是否真的适合自动化测试吗？或者说，这个项目的哪些部分更适合实施自动化测试？

欢迎你给我留言。

05 | 你知道软件开发各阶段都有哪些自动化测试技术吗？

在前面的文章中，我介绍了为什么要做自动化测试，以及什么样的项目适合做自动化测试，那么现在我来说说软件开发生命周期的各个阶段都有哪些类型的自动化测试技术。

说到自动化测试，你可能最为熟悉的就是 GUI 自动化测试了。比如，早年的 C/S 架构，通常就是用自动化测试脚本打开被测应用，然后在界面上以自动化的方式执行一系列的操作；再比如，现今的 Web 站点测试，也是用自动化测试脚本打开浏览器，然后输入要访问的网址，之后用自动化脚本识别定位页面元素，并进行相应的操作。

因此，说到自动化测试时，你的第一反应很可能就是 GUI 自动化测试。然而，在软件研发生命周期的各个阶段都有自动化测试技术的存在，并且对提升测试效率有着至关重要的作用。

今天这篇文章，我将会以不同的软件开发阶段涉及的自动化测试技术为主线，带你了解单元测试、代码级集成测试、Web Service 测试和 GUI 测试阶段的自动化技术，希望可以帮助你更深入地理解“自动化测试”的内涵以及外延。

单元测试的自动化技术

首先，你可能认为单元测试本身就是自动化的，因为它根据软件详细设计采用等价类划分和边界值分析方法设计测试用例，在测试代码实现后再以自动化的方式统一执行。

这个观点非常正确，但这仅仅是一部分，并没有完整地描述单元测试“自动化”的内涵。从广义上讲，单元测试阶段的“自动化”内涵不仅仅指测试用例执行的自动化，还应该包含以下五个方面：

用例框架代码生成的自动化；

部分测试输入数据的自动化生成；

自动桩代码的生成；

被测代码的自动化静态分析；

测试覆盖率的自动统计与分析。

你可能感觉这些内容有些陌生，不过没关系，下面我就详细地跟你说说每一条的具体含义。

第一，用例框架代码生成的自动化

有些框架代码应该由自动化工具生成，而不是由开发者手工完成。这样一来，单元测试开发者可以把更多的精力放在测试逻辑的覆盖和测试数据的选择上，从而大幅提高单元测试用例的质量和开发效率。

TestNG 框架代码应该由自动化工具生成

第二，部分测试输入数据的自动化生成

这部分是指，自动化工具能够根据不同变量类型自动生成测试输入数据。自动化工具本身不可能明白代码逻辑，你可能很难理解它是如何根据需要测试的代码逻辑生成合适的输入数据，并且去判断预计的测试结果的。那我给你举个例子，你就很容易明白了。

比如，某个被测函数的原型是 void fun（int* p, short b），那么测试数据自动生成技术就会为输入参数 int* p 自动生成“空”和“非空”的两个指针 p，然后分别执行函数 void fun（int* p, short b），并观察函数的执行情况。

如果函数内部没有对空指针进行特殊处理，那么函数 fun 的调用必定会抛出异常，从而发现函数的设计缺陷。同样地，对于输入参数 short b 会自动生成超出 short 范围的 b，测试函数 fun 的行为。

第三，自动桩代码的生成

简单地说，桩代码（stub code）是用来代替真实代码的临时代码。 比如，某个函数 A 的内部实现中调用了一个尚未实现的函数 B，为了对函数 A 的逻辑进行测试，那么就需要模拟一个函数 B，这个模拟的函数 B 实现就是所谓的桩代码。

自动桩代码的生成是指自动化工具可以对被测试代码进行扫描分析，自动为被测函数内部调用的其他函数生成可编程的桩代码，并提供基于测试用例的桩代码管理机制。此时，单元测试开发者只需重点关注桩代码内的具体逻辑实现，以及桩代码的返回值。

必要的时候，自动化工具还需要实现 “抽桩”，以适应后续的代码级集成测试的需求。

那什么是“抽桩”呢？其实也很简单，在单元测试阶段，假如函数 A 内部调用的函数 B 是桩代码，那么在代码级集成测试阶段，我们希望函数 A 不再调用假的函数 B，而是调用真实的函数 B，这个用真实函数 B 代替原本桩代码函数 B 的操作，就称为“抽桩”。

第四，被测代码的自动化静态分析

静态分析主要指代码的静态扫描，目的是识别出违反编码规则或编码风格的代码行。通常这部分工作是结合项目具体的编码规则和编码风格，由自动化工具通过内建规则和用户自定义规则自动化完成的。目前比较常用的代码静态分析工具有 Sonar 和 Coverity 等。

严格意义上讲，静态分析不属于单元测试的范畴，但这部分工作一般是在单元测试阶段通过自动化工具完成的，所以我也把它归入到了单元测试自动化的范畴。

第五，测试覆盖率的自动统计与分析

单元测试用例执行结束后，自动化工具可以自动统计各种测试覆盖率，包括代码行覆盖率、分支覆盖率、MC/DC 覆盖率等。这些自动统计的指标，可以帮你衡量单元测试用例集合的充分性和完备性，并可以为你提供适当增补测试用例以提高测试覆盖率的依据。

代码级集成测试的自动化技术

通俗地讲，代码级集成测试是指将已经开发完成的软件模块放在一起测试。

从测试用例设计和测试代码结构来看，代码级集成测试和单元测试非常相似，它们都是对被测试函数以不同的输入参数组合进行调用并验证结果，只不过代码级集成测试的关注点，更多的是软件模块之间的接口调用和数据传递。

代码级集成测试与单元测试最大的区别只是，代码级集成测试中被测函数内部调用的其他函数必须是真实的，不允许使用桩代码代替，而单元测试中允许使用桩代码来模拟内部调用的其他函数。

以上的这些异同点就决定了代码级集成测试“自动化”的内涵与单元测试非常相似，尤其是在实际操作层面，比如测试用例的设计方法、测试用例的代码结构以及数据驱动思想的应用等等。

但是，代码级集成测试对测试框架的要求非常高，这个框架除了可以顺利装载自己的软件模块外，还必须能装载其他相互依赖的模块，做到被测软件模块可运行（Runnable）。

由于代码级集成测试主要应用在早期非互联网的传统软件企业，那时候的软件以“单体”应用居多，一个软件内部包含大量的功能，每一个软件功能都是通过不同的内部模块来实现的，那么这些内部模块在做集成的时候，就需要做代码级集成测试。

现在的开发理念追求的是系统复杂性的解耦，会去尽量避免“大单体”应用，采用 Web Service 或者 RPC 调用的方式来协作完成各个软件功能。所以现在的软件企业，尤其是互联网企业，基本不会去做代码级集成测试，我在这里也就不再进一步展开了。

Web Service 测试的自动化技术

Web Service 测试，主要是指 SOAP API 和 REST API 这两类 API 测试，最典型的是采用 SoapUI 或 Postman 等类似的工具。但这类测试工具基本都是界面操作手动发起 Request 并验证 Response，所以难以和 CI/CD 集成，于是就出现了 API 自动化测试框架。

如果采用 API 自动化测试框架来开发测试用例，那么这些测试用例的表现形式就是代码。为了让你更直观地理解基于代码的 API 测试用例是什么样子的，我给你举一个“创建用户”API 的例子，你只需要看代码的大致步骤就可以了，具体到每行代码的含义，我会在后续文章中详细讲解。

基于 API 自动化测试框架的测试用例示例（测试 CreateUser API）

对于基于代码的 API 测试用例，通常包含三大步骤：

准备 API 调用时需要的测试数据；

准备 API 的调用参数并发起 API 的调用；

验证 API 调用的返回结果。

目前最流行的 API 自动测试框架是 REST Assured，它可以方便地发起 Restful API 调用并验证返回结果。关于 REST Assured 的用法和优点，我会在后续文章中详细介绍。

同样地，Web Service 测试“自动化”的内涵不仅仅包括 API 测试用例执行的自动化，还包括以下四个方面：

测试脚手架代码的自动化生成；

部分测试输入数据的自动生成；

Response 验证的自动化；

基于 SoapUI 或者 Postman 的自动化脚本生成。

接下来，我会依次为你解释这 4 个方面代表什么含义。

第一，测试脚手架代码的自动化生成
和单元测试阶段的用例框架代码自动生成一个道理，你在开发 API 测试的过程中更关心的是，如何设计测试用例的输入参数以及组合，以及在不同参数组合情况下 Response 的验证，而你不希望将精力浪费在代码层面如何组织测试用例、测试数据驱动如何实现等非测试业务上。

这时，测试脚手架代码的自动生成技术就派上用场了。它生成的测试脚手架代码，通常包含了被测试 API 的调用、测试数据与脚本的分离，以及 Response 验证的空实现。

第二，部分测试输入数据的自动生成

这一点和单元测试的测试输入数据的自动化生成也很类似，唯一不同的是，单元测试针对的参数是函数输入参数和函数内部输入，而 API 测试对应的是 API 的参数以及 API 调用的 Payload。数据生成的原则同样遵循边界值原则。

第三，Response 验证的自动化

对于 API 调用返回结果的验证，通常关注的点是返回状态码（status code）、Scheme 结构以及具体的字段值。如果你写过这种类型的测试用例，那你就会知道字段值的验证相当麻烦，只有那些你明确写了 assert 的字段才会被验证，但是通常你不可能针对所有的字段都写 assert，这时就需要 Response 验证的自动化技术了。

Response 验证自动化的核心思想是自动比较两次相同 API 调用的返回结果，并自动识别出有差异的字段值，比较过程可以通过规则配置去掉诸如时间戳、会话 ID（Session ID）等动态值。 这部分内容，我会在后续文章中详细讲解。

第四，基于 SoapUI 或者 Postman 的自动化脚本生成

你在使用 SoapUI 或者 Postman 等工具进行 Web Service 测试时，已经在这些工具里面积累了很多测试用例。那么，在引入了基于代码实现的 API 测试框架之后，就意味着需要把这些测试用例都用代码的方式重写一遍，而这额外的工作量是很难被接受的。

我的建议是，开发一个自动化代码转换生成工具。这个工具的输入是 SoapUI 或者 Postman 的测试用例元数据（即测试用例的 JSON 元文件），输出是符合 API 测试框架规范的基于代码实现的测试用例。这样一来，原本的测试用例积累可以直接转换成在 CI/CD 上可以直接接入的自动化测试用例。

对于新的测试用例，还可以继续用 SoapUI 或者 Postman 做初步的测试验证，初步验证没有问题后，直接转换成符合 API 测试框架规范的测试用例。对于复杂的测试用例，也可以直接基于代码来实现，而且灵活性会更好。

GUI 测试的自动化技术

GUI 测试的自动化技术可能是你最熟悉的，也是发展时间最长、应用最广的自动化测试技术。它的核心思想是，基于页面元素识别技术，对页面元素进行自动化操作，以模拟实际终端用户的行为并验证软件功能的正确性。

目前，GUI 自动化测试主要分为两大方向，传统 Web 浏览器和移动端原生应用（Native App）的 GUI 自动化。虽然二者采用的具体技术差别很大，但是用例设计的思路类似。

对于传统 Web 浏览器的 GUI 自动化测试，业内主流的开源方案采用 Selenium，商业方案采用 Micro Focus 的 UFT（前身是 HP 的 QTP）；

对于移动端原生应用，通常采用主流的 Appium，它对 iOS 环境集成了 XCUITest，对 Android 环境集成了 UIAutomator 和 Espresso。

这部分内容，我会在后续的文章中详细展开。

总结

我给你梳理了软件研发生命周期各个阶段的自动化测试技术，包括单元测试、代码级集成测试、Web Service 测试和 GUI 测试的自动化技术，并给你归纳了每一类技术的核心方法和应用场景。

我希望你通过这篇文章，可以先对自动化测试的全局有一个比较清晰的认识，然后在后续的文章中我还会针对这些技术展开讨论，并给你分享一些相应的实际案例。

思考题

你现在所在的公司，是否实行代码级测试，用到了哪些自动化测试技术？

欢迎你给我留言。

06 | 你真的懂测试覆盖率吗？

在上一篇文章中，我为你介绍了软件测试各个阶段的自动化技术，在前面的文章中我也提到了测试覆盖率的概念，你当时可能有点不明白，那么今天我就和你详细聊聊测试覆盖率这个主题。

测试覆盖率通常被用来衡量测试的充分性和完整性，从广义的角度来讲，测试覆盖率主要分为两大类，一类是面向项目的需求覆盖率，另一类是更偏向技术的代码覆盖率。

需求覆盖率

需求覆盖率是指测试对需求的覆盖程度，通常的做法是将每一条分解后的软件需求和对应的测试建立一对多的映射关系，最终目标是保证测试可以覆盖每个需求，以保证软件产品的质量。

我们通常采用 ALM，Doors 和 TestLink 等需求管理工具来建立需求和测试的对应关系，并以此计算测试覆盖率。

需求覆盖率统计方法属于传统瀑布模型下的软件工程实践，传统瀑布模型追求自上而下地制定计划、分析需求、设计软件、编写代码、测试和运维等，在流程上是重量级的，已经很难适应当今互联网时代下的敏捷开发实践。

所以，互联网测试项目中很少直接基于需求来衡量测试覆盖率，而是将软件需求转换成测试需求，然后基于测试需求再来设计测试点。

因此，现在人们口中的测试覆盖率，通常默认指代码覆盖率，而不是需求覆盖率。

代码覆盖率

简单来说，代码覆盖率是指，至少被执行了一次的条目数占整个条目数的百分比。

如果“条目数”是语句，对应的就是代码行覆盖率；如果“条目数”是函数，对应的就是函数覆盖率；如果“条目数”是路径，那么对应的就是路径覆盖率。依此类推，你就可以得到绝大多数常见的代码覆盖率类型的定义。

这里我给你简单介绍一下最常用的三种代码覆盖率指标。

行覆盖率又称为语句覆盖率，指已经被执行到的语句占总可执行语句（不包含类似 C++ 的头文件声明、代码注释、空行等等）的百分比。这是最常用也是要求最低的覆盖率指标。实际项目中通常会结合判定覆盖率或者条件覆盖率一起使用。

判定覆盖又称分支覆盖，用以度量程序中每一个判定的分支是否都被测试到了，即代码中每个判断的取真分支和取假分支是否各被覆盖至少各一次。比如，对于 if(a>0 && b>0)，就要求覆盖“a>0 && b>0”为 TURE 和 FALSE 各一次。

条件覆盖是指，判定中的每个条件的可能取值至少满足一次，度量判定中的每个条件的结果 TRUE 和 FALSE 是否都被测试到了。比如，对于 if(a>0 && b>0)，就要求“a>0”取 TRUE 和 FALSE 各一次，同时要求“b>0”取 TRUE 和 FALSE 各一次。

代码覆盖率的价值

现在很多项目都在单元测试以及集成测试阶段统计代码覆盖率，但是我想说的是，统计代码覆盖率仅仅是手段，你必须透过现象看到事物的本质，才能从根本上保证软件整体的质量。

统计代码覆盖率的根本目的是找出潜在的遗漏测试用例，并有针对性的进行补充，同时还可以识别出代码中那些由于需求变更等原因造成的不可达的废弃代码。

通常我们希望代码覆盖率越高越好，代码覆盖率越高越能说明你的测试用例设计是充分且完备的，但你也会发现测试的成本会随着代码覆盖率的提高以类似指数级的方式迅速增加。

如果想达到 70% 的代码覆盖率，你可能只需要 30 分钟的时间成本。但如果你想把代码覆盖率提高到 90%，那么为了这额外的 20%，你可能花的时间就远不止 30 分钟了。更进一步，你如果想达到 100% 的代码覆盖率，可想而知你花费的代价就会更大了。

那么，为什么代码覆盖率的提高，需要付出越来越大的代价呢？因为在后期，你需要大量的桩代码、Mock 代码和全局变量的配合来控制执行路径。

所以，在软件企业中，只有单元测试阶段对代码覆盖率有较高的要求。因为从技术实现上讲，单元测试可以最大化地利用打桩技术来提高覆盖率。而你如果想在集成测试或者是 GUI 测试阶段将代码覆盖率提高到一定量级，那你所要付出的代价是巨大的，而且在很多情况下根本就实现不了。

代码覆盖率的局限性

我先来问你一个问题，如果你通过努力，已经把某个函数的 MC/DC 代码覆盖率（MC/DC 覆盖率是最高标准的代码覆盖率指标，除了直接关系人生命安全的软件以外，很少会有项目会有严格的 MC/DC 覆盖率要求）做到了 100%，软件质量是否就真的高枕无忧、万无一失了呢？

很不幸，即使你所设计的测试用例已经达到 100% 的代码覆盖率，软件产品的质量也做不到万无一失。其根本原因在于代码覆盖率的计算是基于现有代码的，并不能发现那些“未考虑某些输入”以及“未处理某些情况”形成的缺陷。

我给你举个极端的例子，如果一个被测函数里面只有一行代码，只要这个函数被调用过了，那么衡量这一行代码质量的所有覆盖率指标都会是 100%，但是这个函数是否真正实现了应该需要实现的功能呢？

显然，代码覆盖率反映的仅仅是已有代码的哪些逻辑被执行过了，哪些逻辑还没有被执行过。以此为依据，你可以补充测试用例，可以去测试那些还没有覆盖到的执行路径。但也是仅此而已，对于那些压根还没有代码实现的部分，基于代码覆盖率的统计指标就无能为力了。

总结来讲，高的代码覆盖率不一定能保证软件的质量，但是低的代码覆盖率一定不能能保证软件的质量。

好了，现在你已经了解了代码覆盖率的概念、价值和局限性，那么接下来，我就以 Java 代码覆盖率工具为例，给你解释一下代码覆盖率工具的内部实现原理以及一些关键技术。

当你理解了这部分内容，以后再面对各个不同开发语言的不同代码覆盖率工具时，就可以做到胸有成竹地根据具体的项目性质，选择最合适的代码覆盖率工具了。

代码覆盖率工具

JaCoCo 是一款 Java 代码的主流开源覆盖率工具，可以很方便地嵌入到 Ant、Maven 中，并且和很多主流的持续集成工具以及代码静态检查工具，比如 Jekins 和 Sonar 等，都有很好的集成。

首先，我先带你看看 JaCoCo 的代码覆盖率报告长什么样子。

如图 1 所示为 JaCoCo 的整体代码覆盖率统计报告，包括了每个 Java 代码文件的行覆盖率以及分支覆盖率统计，并给出了每个 Java 代码文件的行数、方法数和类数等具体信息。

图 1 JaCoCo 代码覆盖率统计报告实例

如图 2 所示为每个 Java 文件内部详细的代码覆盖率情况，图中绿色的行表示已经被覆盖，红色的行表示尚未被覆盖，黄色的行表示部分覆盖；左侧绿色菱形块表示该分支已经被完全覆盖、黄色菱形块表示该分支仅被部分覆盖。

图 2 JaCoCo 详细代码覆盖率实例

显然，通过这个详尽的报告，你就可以知道代码真实的执行情况、哪些代码未被覆盖。以此为基础，你再去设计测试用例就会更有针对性了。

代码覆盖率工具的实现原理

JaCoCo 的详细报告，让你惊叹于代码覆盖率工具的强大。但你有没有仔细想过，这样的统计信息如何被获取到的呢？

图 3 统计代码覆盖率的不同注入实现技术

实现代码覆盖率的统计，最基本的方法就是注入（Instrumentation）。简单地说，注入就是在被测代码中自动插入用于覆盖率统计的探针（Probe）代码，并保证插入的探针代码不会给原代码带来任何影响。

对于 Java 代码来讲，根据注入目标的不同，可以分为源代码（Source Code）注入和字节码（Byte Code）注入两大类。基于 JVM 本身特性以及执行效率的原因，目前主流的工具基本都是使用字节码注入，注入的具体实现采用 ASM 技术。

ASM 是一个 Java 字节码操纵框架，能被用来动态生成类或者增强既有类的功能，可以直接产生 class 文件，也可以在类被加载入 JVM 之前动态改变类行为。

根据注入发生的时间点，字节码注入又可以分为两大模式：On-The-Fly 注入模式和 Offline 注入模式。

第一，On-The-Fly 注入模式

On-The-Fly 模式的特点在于无需修改源代码，也无需提前进行字节码插桩。它适用于支持 Java Agent 的运行环境。

这样做的优点是，可以在系统不停机的情况下，实时收集代码覆盖率信息。缺点是运行环境必须允许使用 Java Agent。

实现 On-The-Fly 模式，主要有两种技术方案：

开发自定义的类装载器（Class Loader）实现类装载策略，每次类加载前，需要在 class 文件中插入探针，早期的 Emma 就是使用这种方案实现的探针插入；

借助 Java Agent，利用执行在 main() 方法之前的拦截器方法 premain() 来插入探针，实际使用过程中需要在 JVM 的启动参数中添加“-javaagent”并指定用于实时字节码注入的代理程序，这样代理程序在装载每个 class 文件前，先判断是否已经插入了探针，如果没有则需要将探针插入 class 文件中，目前主流的 JaCoCo 就是使用了这个方式。

第二，Offline 注入模式

Offline 模式也无需修改源代码，但是需要在测试开始之前先对文件进行插桩，并事先生成插过桩的 class 文件。它适用于不支持 Java Agent 的运行环境，以及无法使用自定义类装载器的场景。

这样做的优点是，JVM 启动时不再需要使用 Java Agent 额外开启代理，缺点是无法实时获取代码覆盖率信息，只能在系统停机时下获取。

Offline 模式根据是生成新的 class 文件还是直接修改原 class 文件，又可以分为 Replace 和 Inject 两种不同模式。

和 On-The-Fly 注入模式不同，Replace 和 Inject 的实现是，在测试运行前就已经通过 ASM 将探针插入了 class 文件，而在测试的运行过程中不需要任何额外的处理。Cobertura 就是使用 Offline 模式的典型代表。

总结

测试覆盖率通常被用来衡量测试的充分性和完整性，包括面向项目的需求覆盖率和更偏向技术的代码覆盖率。而需求覆盖率的统计方式不再适用于现在的敏捷开发模式，所以现在谈到测试覆盖率，大多是指代码覆盖率。

但是，高的代码覆盖率不一定能保证软件的质量，因为代码覆盖率是基于现有代码，无法发现那些“未考虑某些输入”以及“未处理某些情况”形成的缺陷。

另外，对于代码覆盖率的统计工具，我希望你不仅仅是会用的层次，而是能够理解它们的原理，知其然知其所以然，才能更好地利用这些工具完成你的测试工作。

思考题

你在实际工作中，是否还接触过 C/C++，JavaScript 等语言的代码覆盖率工具，比如 GCC Coverage、JSCoverage 和 Istanbul 等？如果接触过的话，请你谈谈自己使用的感受以及遇到过的“坑”。

欢迎你给我留言。

07 | 如何高效填写软件缺陷报告？

在上一篇文章中，我为你介绍了测试覆盖率的概念，并重点介绍了代码覆盖率的应用价值以及局限性。今天我会为你介绍如何才能写出一份高效的软件缺陷报告。

测试工程师需要利用对需求的理解、高效的执行力以及严密的逻辑推理能力，迅速找出软件中的潜在缺陷，并以缺陷报告的形式递交给开发团队，这看起来是不是有点像侦探柯南呢。

缺陷报告是测试工程师与开发工程师交流沟通的重要桥梁，也是测试工程师日常工作的重要输出。 作为优秀的测试工程师，最基本的一项技能就是，把发现的缺陷准确无歧义地表达清楚。

“准确无歧义地表达”意味着，开发工程师可以根据缺陷报告快速理解缺陷，并精确定位问题。同时，通过这个缺陷报告，开发经理可以准确预估缺陷修复的优先级、产品经理可以了解缺陷对用户或业务的影响以及严重性。

可见，缺陷报告本身的质量将直接关系到缺陷被修复的速度以及开发工程师的效率，同时还会影响测试工程师的信用、测试与开发人员协作的有效性。

那么，如何才能写出一份高效的缺陷报告呢？或者说，一份好的缺陷报告需要包括哪些具体内容呢？

你可能觉得这并不是什么难事儿，毕竟软件企业通常都有缺陷管理系统，比如典型的 ALM（以前的 Quality Center）、JIRA、Bugzilla、BugFree 和 Mantis 等。当使用这类系统递交缺陷时，会自动生成模板，你只要按照其中的必填字段提供缺陷的详细信息就可以了。

很多时候，你不用想应该提供些什么信息，系统会引导你提供相关的信息。但是，你有仔细想过为什么要填写这些字段，这些字段都起什么作用，以及每个字段的内容具体应该怎么填写吗？

你必须牢牢记住的是，好的缺陷报告绝对不是大量信息的堆叠，而是以高效的方式提供准确有用的信息。

接下来，我就带你一起去看一份高效的缺陷报告主要由哪些部分组成，以及每部分内容的关键点是什么。

缺陷标题

缺陷标题通常是别人最先看到的部分，是对缺陷的概括性描述，通常采用“在什么情况下发生了什么问题”的模式。

首先，对“什么问题”的描述不仅要做到清晰简洁，最关键是要足够具体，切忌不能采用过于笼统的描述。描述“什么问题”的同时还必须清楚地表述发生问题时的上下文，也就是问题出现的场景。

“用户不能正常登录”“搜索功能有问题”和“用户信息页面的地址栏位置不正确”等，这样的描述会给人“说了等于没说”的感觉。这样的描述，很容易引发开发工程师的反感和抵触情绪，从而造成缺陷被拒绝修改（reject）。同时，还会造成缺陷管理上的困难以及过程的低效。

比如，当你发现了一个菜单栏上某个条目缺失的问题，在递交缺陷报告前，通常会去缺陷管理系统搜索一下是否已经有人递交过类似的缺陷。

当你以“菜单栏”为关键字搜索时，你可能会得到一堆“菜单栏有问题”的缺陷，如果缺陷标题的描述过于笼统，你就不得不点击进入每个已知缺陷点去看细节描述，这就会大大降低你的工作效率。

所以，如果缺陷标题本身就能概括性地描述具体问题，你就可以通过阅读标题判断类似的缺陷是否被提交过，大大提高测试工程师提交缺陷报告的效率。

其次，标题应该尽可能描述问题本质，而避免只停留在问题的表面。

比如，“商品金额输入框，可以输入英文字母和其他字符”这个描述就只描述了问题的表面现象，而采用诸如“商品金额输入框，没有对输入内容做校验”的方式，就可以透过标题看到缺陷的本质，这样可以帮助开发人员快速掌握问题的本质。

最后，缺陷标题不易过长，对缺陷更详细的描述应该放在“缺陷概述”里。

缺陷概述

缺陷概述通常会提供更多概括性的缺陷本质与现象的描述，是缺陷标题的细化。这部分内容通常是开发工程师打开缺陷报告后最先关注的内容，所以用清晰简短的语句将问题的本质描述清楚是关键。

缺陷概述还会包括缺陷的其他延展部分，比如你可以在这部分列出同一类型的缺陷可能出现的所有场景；再比如，你还可以描述同样的问题是否会在之前的版本中重现等。在这里，你应该尽量避免以缺陷重现步骤的形式来描述，而应该使用概括性的语句。

总之，缺陷概述的目的是，清晰简洁地描述缺陷，使开发工程师能够聚焦缺陷的本质。

缺陷影响

缺陷影响描述的是，缺陷引起的问题对用户或者对业务的影响范围以及严重程度。

缺陷影响决定了缺陷的优先级（Priority）和严重程度（Severity），开发经理会以此为依据来决定修复该缺陷的优先级；而产品经理会以此为依据来衡量缺陷的严重程度，并决定是否要等该缺陷被修复后才能发布产品。

测试工程师准确描述缺陷影响的前提是，必须对软件的应用场景以及需求有深入的理解，这也是对测试工程师业务基本功的考验。

环境配置

环境配置用以详细描述测试环境的配置细节，为缺陷的重现提供必要的环境信息。 比如，操作系统的类型与版本、被测软件版本、浏览器的种类和版本、被测软件的配置信息、集群的配置参数、中间件的版本信息等等。

需要注意的是，环境配置的内容通常是按需描述，也就是说通常只描述那些重现缺陷的环境敏感信息。

比如，“菜单栏上某个条目缺失的问题”只会发生在 Chrome 浏览器，而其他浏览器都没有类似问题。那么，Chrome 浏览器就是环境敏感信息，必须予以描述，而至于 Chrome 浏览器是运行在什么操作系统上就无关紧要了，无需特意去描述了。

前置条件

前置条件是指测试步骤开始前系统应该处在的状态，其目的是减少缺陷重现步骤的描述。合理地使用前置条件可以在描述缺陷重现步骤时排除不必要的干扰，使其更有针对性。

比如，某个业务操作需要先完成用户登录，你在缺陷重现步骤里就没有必要描述登录操作的步骤细节，可以直接使用 “前置条件：用户已完成登录”的描述方式；

再比如，用户在执行登录操作前，需要事先在被测系统准备好待登录用户，你在描述时也无需增加“用测试数据生成工具生成用户”的步骤，可以直接使用 “前置条件：用户已完成注册”的描述方式。

缺陷重现步骤

缺陷重现步骤是整个缺陷报告中最核心的内容，其目的在于用简洁的语言向开发工程师展示缺陷重现的具体操作步骤。

这里需要注意的是，操作步骤通常是从用户角度出发来描述的，每个步骤都应该是可操作并且是连贯的，所以往往会采用步骤列表的表现形式。

通常测试工程师在写缺陷重现步骤前，需要反复执行这些步骤 3 次以上：一是，确保缺陷的可重现性；二是，找到最短的重现路径，过滤掉那些非必要的步骤，避免产生不必要的干扰。

对于缺陷重现步骤的描述应该尽量避免以下 3 个常见问题：

笼统的描述，缺乏可操作的具体步骤。

出现与缺陷重现不相关的步骤。

缺乏对测试数据的相关描述。

期望结果和实际结果

期望结果和实际结果通常和缺陷重现步骤绑定在一起，在描述重现步骤的过程中，需要明确说明期待结果和实际结果。期待结果来自于对需求的理解，而实际结果来自于测试执行的结果。

通常来讲，当你描述期望结果时，需要说明应该发生什么，而不是什么不应该发生；而描述实际结果时，你应该说明发生了什么，而不是什么没有发生。

优先级（Priority）和严重程度（Severity）

我之所以将优先级和严重程度放在一起，是因为这两个概念看起来有点类似，而本质却完全不同。而且，很多入行不久的测试工程师，也很难搞清楚这两者的差异到底在哪里。

根据百度百科的解释，缺陷优先级是指缺陷必须被修复的紧急程度，而缺陷严重程度是指因缺陷引起的故障对软件产品的影响程度。

可见，严重程度是缺陷本身的属性，通常确定后就不再变化，而优先级是缺陷的工程属性，会随着项目进度、解决缺陷的成本等因素而变动。那么，缺陷的优先级和严重程度又有什么关系呢？

缺陷越严重，优先级就越高；

缺陷影响的范围越大，优先级也会越高；

有些缺陷虽然从用户影响角度来说不算严重，但是会妨碍测试或者是自动化测试的执行，这类缺陷属于典型的严重程度低，但是优先级高；

有些缺陷虽然严重程度比较高，但是考虑到修复成本以及技术难度，也会出现优先级较低的情况。

变通方案（Workaround）

变通方案是提供一种临时绕开当前缺陷而不影响产品功能的方式，通常由测试工程师或者开发工程师完成，或者他们一同决定。

变通方案的有无以及实施的难易程度，是决定缺陷优先级和严重程度的重要依据。如果某个严重的缺陷没有任何可行的变通方案，那么不管修复缺陷代价有多大，优先级一定会是最高的，但是如果该缺陷存在比较简单的变通方案，那么优先级就不一定会是最高的了。

根原因分析（Root Cause Analysis）

根原因分析就是我们平时常说的 RCA，如果你能在发现缺陷的同时，定位出问题的根本原因，清楚地描述缺陷产生的原因并反馈给开发工程师，那么开发工程师修复缺陷的效率就会大幅提升，而且你的技术影响力也会被开发认可。

可以做好根原因分析的测试工程师，通常都具有开发背景，或者至少有较好的代码阅读以及代码调试的能力。

所以做为测试工程师，你很有必要去深入学习一门高级语言，这将帮助你体系化地建立起编程思想和方法，这样在之后的工作中，无论你是面对开发的代码，还是自动化测试代码和脚本都能做到得心应手，应对自如。

附件（Attachment）

附件通常是为缺陷的存在提供必要的证据支持，常见的附件有界面截图、测试用例日志、服务器端日志、GUI 测试的执行视频等。

对于那些很难用文字描述清楚的 GUI 界面布局的缺陷，你可以采用截图并高亮显示应该关注的区域的方式去提交缺陷报告。

总结

缺陷报告是测试工程师与开发工程师交流沟通的重要桥梁，也是测试工程师日常工作的重要输出。

一份高效的软件缺陷报告，应该包括缺陷标题、缺陷概述、缺陷影响、环境配置、前置条件、缺陷重现步骤、期望结果和实际结果、优先级和严重程度、变通方案、根原因分析，以及附件这几大部分。

缺陷报告的每一部分内容，都会因为目的、表现形式有各自的侧重点，所以想要写出一份高效的软件缺陷报告，需要对其组成有深入的理解。

思考题

关于高效填写软件缺陷报告，你还有哪些好的实践？

欢迎你给我留言。

08 | 以终为始，如何才能做好测试计划？

在上一篇文章中，我为你介绍了如何高效填写软件缺陷报告，并为你解读了缺陷报告中的关键内容。今天，我将为你介绍一份成功的测试计划应该包含哪些内容，以及如何才能做好测试计划。

软件项目，通常都会有详细的项目计划。软件测试作为整个项目中的重要一环，也要执行详细的测试计划。正所谓运筹帷幄之中，决胜千里之外，强调的就是预先计划的重要性和必要性。

在早期的软件工程实践中，软件测试计划的制定通常是在需求分析以及测试需求分析完成后开始，并且是整个软件研发生命周期中的重要环节。

但是，在敏捷开发模式下，你可能会有这样的疑问，软件测试计划还有那么重要吗？我所在的软件项目压根儿就没有正式的测试计划，不也没出什么大问题吗？

的确，对于很多非产品型的互联网公司，由于采用了敏捷开发模式，的确很少去制定传统意义上的测试计划了，但这并不是说它们就不再制定测试计划了。

只不过是，测试计划的表现形式已经不再是传统意义上庞大的、正式的测试计划文档了，而更多的是体现在每个迭代（sprint）的计划环节，而且这样的短期测试计划可以非常迅速地根据项目情况实时调整。

所以说，测试计划依旧存在，只是从原来的一次性集中制定测试计划，变成了以迭代的方式持续制定测试计划。 但是对于传统软件企业，或者是做非互联网软件产品的企业，它们通常还是会有非常正式的软件测试计划。

由此可见，无论对于早期最具典型性的瀑布开发模型，还是现在的敏捷开发模型，测试计划的重要性始终没有发生变化。那么，你可能会问，测试计划的重要性到底体现在哪些方面呢？在回答这个问题之前，我先跟你聊聊如果没有测试计划会带来什么问题。

没有测试计划会怎么样？

如果没有测试计划，会带来哪些问题呢？

很难确切地知道具体的测试范围，以及应该采取的具体测试策略；

很难预估具体的工作量和所需要的测试工程师数量，同时还会造成各个测试工程师的分工不明确，引发某些测试工作被重复执行而有些测试则被遗漏的问题；

测试的整体进度完全不可控，甚至很难确切知道目前测试的完成情况，对于测试完成时间就更难预估准确的时间节点了；

整个项目对潜在风险的抵抗能力很弱，很难应对需求的变更以及其他突发事件。

从这些问题中，你可以逆向思维推导出，一份好的测试计划要包括：测试范围、测试策略、测试资源、测试进度和测试风险预估，这五大方面，并且每一部分都要给出应对可能出现问题的解决办法。

测试范围

顾名思义，测试范围描述的是被测对象以及主要的测试内容。

比如，对于用户登录模块，功能测试既需要测试浏览器端又需要测试移动端，同时还考虑登录的安全和并发性能相关的非功能性需求的测试等。

测试范围的确定通常是在测试需求分析完成后进行，所以确定测试范围的过程在一定程度上也是对测试需求分析的进一步检验，这将有助于在早期阶段就发现潜在的测试遗漏。

同时，由于不可能进行穷尽测试，而且测试的时间和资源都是有限的，所以必须有所取舍，进行有针对性的测试。因此，测试范围中需要明确“测什么”和“不测什么”。

测试策略的话题

测试策略简单来讲就是需要明确“先测什么后测什么”和“如何来测”这两个问题。

病有轻重缓急，测试也是一样的道理，重要的项先测，而不重要的项要后测。测试策略会要求我们明确测试的重点，以及各项测试的先后顺序。

比如，对用户登录模块来讲，“用户无法正常登录”和“用户无法重置密码”这两个潜在问题，对业务的影响孰轻孰重一目了然，所以，你应该按照优先级来先测“用户正常登录”，再测“用户重置密码”。

测试策略还需要说明，采用什么样的测试类型和测试方法。 这里需要注意的是，不仅要给出为什么要选用这个测试类型，还要详细说明具体的实施方法。

第一，功能测试

对于功能测试，你应该根据测试需求分析的思维导图来设计测试用例。

主线业务的功能测试由于经常需要执行回归测试，所以你需要考虑实施自动化测试，并且根据项目技术栈和测试团队成员的习惯与能力来选择合适的自动化测试框架。

这里需要注意的是，你通常应该先实现主干业务流程的测试自动化。

实际操作时，你通常需要先列出主要的功能测试点，并决定哪些测试点适合采用自动化测试，并且决定具体使用什么样的框架和技术。

对于需要手工测试的测试点，你要决定采用什么类型的测试用例设计方法，以及如何准备相关的测试数据。

另外，你还要评估被测软件的可测试性，如果有可测试性的问题，需要提前考虑切实可行的变通方案，甚至要求开发人员提供可测试性的接口。

第二，兼容性测试

对于兼容性测试来说，Web 测试需要确定覆盖的浏览器类型和版本，移动设备测试需要确定覆盖的设备类型和具体 iOS/Android 的版本等。

你可能会问，我要怎么确定需要覆盖的移动设备类型以及 iOS/Android 的版本列表呢？这个问题其实并不难：

如果是既有产品，你可以通过大数据技术分析产品的历史数据得出 Top 30% 的移动设备以及 iOS/Android 的版本列表，那么兼容性测试只需覆盖这部分即可。

如果是一个全新的产品，你可以通过 TalkingData 这样的网站来查看目前主流的移动设备，分辨率大小、iOS/Android 版本等信息来确定测试范围。

兼容性测试的实施，往往是在功能测试的后期，也就是说需要等功能基本都稳定了，才会开始兼容性测试。

当然也有特例，比如，对于前端引入了新的前端框架或者组件库，往往就会先在前期做兼容性评估，以确保不会引入后期无法解决的兼容性问题。

兼容性测试用例的选取，往往来自于已经实现的自动化测试用例。道理很简单，因为兼容性测试往往要覆盖最常用的业务场景，而这些最常用的业务场景通常也是首批实现自动化测试的目标。

所以，我们的 GUI 自动化框架，就需要能够支持同一套测试脚本在不做修改的前提下，运行于不同的浏览器。

第三，性能测试

对于性能测试，需要在明确了性能需求（并发用户数、响应时间、事务吞吐量等）的前提下，结合被测系统的特点，设计性能测试场景并确定性能测试框架。

比如，是直接在 API 级别发起压力测试，还是必须模拟终端用户行为进行基于协议的压力测试。再比如，是基于模块进行压力测试，还是发起全链路压测。

如果性能是背景数据敏感的场景，还需要确定背景数据量级与分布，并决定产生背景数据的技术方案，比如是通过 API 并发调用来产生测试数据，还是直接在数据库上做批量 insert 和 update 操作，或者是两种方式的结合。

最后，无论采用哪种方式，都需要明确待开发的单用户脚本的数量，以便后续能够顺利组装压测测试场景。

性能测试的实施，是一个比较复杂的问题。首先，需要根据你想要解决的问题，确定性能测试的类型；然后，根据具体的性能测试类型开展测试。

性能测试的实施，往往先要根据业务场景来决定需要开发哪些单用户脚本，脚本的开发会涉及到很多性能测试脚本特有的概念，比如思考时间、集合点、动态关联等等。

脚本开发完成后，你还要以脚本为单位组织测试场景（Scenario），场景定义简单来说就是百分之多少的用户在做登录、百分之多少的用户在做查询、每个用户的操作步骤之间需要等待多少时间、并发用户的增速是 5 秒一个，还是 5 秒 2 个等等。

最后，才是具体的测试场景执行。和自动化功能测试不同，性能测试执行完成后性能测试报告的解读，是整个测试过程中最关键的点。

如果你现在不太清楚我上面提到的一些概念也没关系，我会在后续的文章中为你详细讲解。

除了我给你详细分析的、最常用的功能测试、兼容性测试和性能测试外，还有很多测试类型（比如，接口测试、集成测试、安全测试、容量验证、安装测试、故障恢复测试等）。这些测试类型，都有各自的应用场景，也相应有独特的测试方法，在这里我就不再一一展开了，如果你有关于这些测试类型的问题，可以给我留言讨论。

测试资源

测试资源通常包括测试人员和测试环境，这两类资源都是有限的。测试计划的目的就是，保证在有限资源下的产出最大化。所以，测试资源就是需要明确“谁来测”和“在哪里测”这两个问题。

测试人员是最重要的，直接关系到整个测试项目的成败和效率。测试人员的资源通常有两个维度：一是，测试工程师的数量；二是，测试工程师的个人经验和能力。

你会发现，测试工程师的经验和能力不足，很难通过测试人员的数量来弥补。相反地，测试工程师的经验和能力都非常强的情况下，测试人员的数量可以适当地减少。

通常在测试团队中，测试工程师既有资深，也会有初级，那么你就必须针对团队的实际情况去安排测试计划。比如，难度较大的工作，或者一些新工具、新方法的应用，又或者自动化测试开发工作，通常由资深的测试工程师来承担；而那些相对机械性、难度较小的工作，则由初级工程师完成。

可见，你要想规划好测试资源，除了要了解项目本身外，还必须对测试团队的人员特点有清晰的把控。另外，我强烈建议你把具体的任务清晰地落实到每个人的身上，这将有利于建立清晰的责任机制，避免后续可能发生的扯皮。

相对于测试人员，测试环境就比较好理解了。不同的项目，可能会使用共享的测试环境，也可能使用专用的测试环境，甚至还会直接使用生产环境。另外，对于目前一些已经实现容器化部署与发布的项目，测试环境就会变得更简单与轻量级，这部分内容我会在后续的文章中给你详细讲解。

测试进度

在明确了测试范围、测试策略和测试资源之后，你就要考虑具体的测试进度了。测试进度主要描述各类测试的开始时间，所需工作量，预计完成时间，并以此为依据来建议最终产品的上线发布时间。

比如，版本接受测试（Build Acceptance Test）的工作量，冒烟测试（Smoke Test）的工作量，自动化脚本开发的工作量，缺陷修复的验证工作量，需要几轮回归测试、每一轮回归测试的工作量等等。

在传统瀑布模型中，测试进度完全依赖于开发完成并递交测试版本的时间。如果开发提交测试版本发生了延误，那么在不裁剪测试需求的情况下，产品整体的上线时间就同样会延期。

然而在敏捷模式下，测试活动贯穿于整个开发过程，很多测试工作会和开发工作同步进行，比如采用行为驱动开发（Behavior-Driven Development）模式，这样测试进度就不会完全依赖于开发递交可测试版本的时间。

行为驱动开发，就是平时我们经常说的 BDD，指的是可以通过自然语言书写非程序员可读的测试用例，并通过 StepDef 来关联基于自然语言的步骤描述和具体的业务操作，最典型的框架就是知名“Cucumber”。

测试风险预估

俗话说，计划赶不上变化，对于测试也是一样的道理，很少有整个测试过程是完全按照原本测试计划执行的。通常需求变更、开发延期、发现重大缺陷和人员变动是引入项目测试风险的主要原因。

对于需求变更，比如增加需求、删减需求、修改需求等，一定要重新进行测试需求分析，确定变更后的测试范围和资源评估，并与项目经理和产品经理及时沟通因此引起的测试进度变化。测试经理 / 测试负责人切忌不能有自己咬牙扛过去的想法，否则无论是对测试团队还是对产品本身都不会有任何好处。

另外，随着测试的开展，你可能会发现前期对于测试工作量的预估不够准确，也可能发现需要增加更多的测试类型，也可能发现因为要修改测试架构的严重缺陷而导致很多的测试需要全回归，还有可能出现开发递交测试版本延期，或者人员变动等各种情况。

所以，在制定测试计划时，你就要预估整个测试过程中可能存在的潜在风险，以及当这些风险发生时的应对策略。 那么，在真的遇到类似问题时，你才可以做到心中不慌，有条不紊地应对这些挑战。

总结

软件测试同软件项目一样，也要制定详细的测试计划。虽然在敏捷开发模式下，软件测试不再局限于厚重的、正式的计划文档，但是测试计划的重要性丝毫没有发生变化。

一份成功的测试计划，必须清楚地描述：测试范围、测试策略、测试资源、测试进度和测试风险预估这五个最重要的方面。

测试范围需要明确“测什么”和“不测什么”；测试策略需要明确“先测什么后测什么”和“如何来测”；测试资源需要明确“谁来测”和“在哪里测”；测试进度是需要明确各类测试的开始时间，所需工作量和预计完成时间；测试风险预估是需要明确如何有效应对各种潜在的变化。

思考题

在这篇文章中，我只和你分享了做好测试计划中最最关键的内容，意在抛砖引玉。那么，你在工程实践中，还有哪些见解呢？

欢迎你给我留言。

09 | 软件测试工程师的核心竞争力是什么？

在前面的文章中，我给你介绍了测试工程师应该具备的一些基础知识，包括如何设计测试用例、如何制定测试计划、什么是测试覆盖率，以及软件生命周期各个阶段的自动化技术等等内容，相信你通过这些文章，或温故知新，或拓展视野，希望你有所收获。

那么，在介绍完这些比较基础的内容后，今天我就来和你聊聊测试工程师的核心竞争力到底什么。只有当你真正明白了自己的核心竞争力，你才能理清“应该做什么”和“应该怎么做”这两个问题，才能朝着正确的方向前行。

我以我们团队招聘功能测试和测试开发工程师为例，带你了解一下测试工程师的核心竞争力到底是什么。

案例一来自我们的资深功能测试工程师招聘。当时，有一位拥有近 9 年测试经验的资深测试候选人，我对他的简历还是比较满意的，所以就安排了面谈。但是，在聊的过程中我很快发现，这位候选人绝大多数的测试经验积累都“强”绑定在特定的业务领域。
如果抛开这个特定的业务领域，他对测试技术本身以及产品技术实现都缺乏系统的思考和理解。换言之，他的价值仅仅能够体现在这个特定的产品业务上，而一旦离开了这个业务领域，他的经验积累很难被有效重用，也就是说他很难快速适应并胜任我们的业务领域测试。所以，他最终没有得到我们的 offer。
从这个案例中，你可以看出作为测试人员，必须要深入理解业务，但是业务知识不能等同于测试能力。

案例二来自我们的测试开发岗位招聘。当时，有一位 5 年测试开发从业经验的候选人，是南京大学软件学院的硕士，毕业后一直在国内的互联网巨头公司从事测试框架和工具平台的开发工作。
看完他的简历，我发现他参与开发过的测试框架和工具和我们当时在做的项目很匹配，加之他的背景也相当不错，内心感觉这个职位基本就是他的了。但是，面谈结束后，我彻底改变了想法。
他所做的的确是测试框架和工具平台的开发工作，但是他的核心能力纯粹就是开发，他只关注如何实现预先设计的功能，而完全不关心所开发的测试框架和工具平台在测试中的具体应用场景。
我承认他的开发能力，但他并不能胜任我们的测试开发岗位。因为，测试开发岗位的核心其实是“测试”，“开发”的目的是更好地服务于测试，我们看重的是对测试的理解，以及在此基础上设计、开发帮助测试人员提高效率并解决实际问题的工具，而不是一个按部就班、纯粹意义上的开发人员。

这两个实际案例，是否已经引发你去思考这样一个问题：什么才是测试工程师的核心竞争力？

目前的测试工程师分为两大类别，一类是做业务功能测试的，另一类是做测试开发的，二者的核心竞争力有很大差别。那么，接下来我就带你一起去看看，功能测试和测试开发工程师的核心竞争力分别是什么。

我先带你看看业务功能测试工程师，也就是传统意义上的测试工程师的核心竞争力，我归纳了以下几点。

传统测试工程师师应该具备的核心竞争力

这部分内容，我按照一项能力对测试工程师的重要程度的顺序，给你依次归纳了测试工程师要具备的七项核心竞争力，包括：测试策略设计能力、测试用例设计能力、快速学习能力、探索性测试思维、缺陷分析能力、自动化测试技术和良好的沟通能力。

或许，你感觉测试策略设计能力、探索性测试思维等对资深的测试工程师来说更重要，而你现在还处在培养快速学习能力、沟通能力、测试用例设计能力的维度。那也没有关系，不断地学习、丰富自己的知识体系，具备更强的职场竞争力，不正是你在追求的吗？

所以，我在分析你应该具备的主要能力的同时，也会给你分享如何才能使自己具备这些能力 ，帮你成就更好的自己。

第一项核心竞争力，测试策略设计能力

测试策略设计能力是指，对于各种不同的被测软件，能够快速准确地理解需求，并在有限的时间和资源下，明确测试重点以及最适合的测试方法的能力。

具备出色的测试策略设计能力，你可以非常明确地回答出测试过程中遇到的这些关键问题：

测试要具体执行到什么程度；

测试需要借助于什么工具；

如何运用自动化测试以及自动化测试框架，以及如何选型；

测试人员资源如何合理分配；

测试进度如何安排；

测试风险如何应对。

培养出色的测试策略设计能力，不是一朝一夕的事情，通常需要经过大量项目的实际历练，并且你还要保持持续思考，主动去提炼共性的内容。

不像测试技术，你可以通过培训或者网上资料的学习而有快速的提升，测试策略设计能力一定是需要你在大量实践的基础上潜移默化形成的。

我认为，测试策略设计能力是功能测试工程师最核心的竞争力，也是最难培养的。

第二项核心竞争力，测试用例设计能力

测试用例设计能力是指，无论对于什么类型的测试，都能设计出高效地发现缺陷，保证产品质量的优秀测试用例。

要做好测试用例设计，不仅需要深入理解被测软件的业务需求和目标用户的使用习惯，还要熟悉软件的具体设计和运行环境，包括技术架构、缓存机制、中间件技术、第三方服务集成等等。

测试用例设计能力要求你不仅仅局限于熟悉业务领域的测试用例设计，而是能够融会贯通，熟练地把系统性的测试设计方法和具体业务有机结合，对任何被测软件都可以输出出色的测试用例。

要想提高测试用例设计能力，你平时就要多积累，对常见的缺陷模式、典型的错误类型以及遇到过的缺陷，要不断地总结、归纳，才能逐渐形成体系化的用例设计思维。

同时，你还可以阅读一些好的测试用例设计实例开阔思路，日后遇到类似的被测系统时，可以做到融会贯通和举一反三。

第三项核心竞争力，快速学习能力

快速学习能力，包含两个层面的含义：

对不同业务需求和功能的快速学习与理解能力；

对于测试新技术和新方法的学习与应用能力。

显然，快速学习能力是各行业从业者应该具备的能力，但为什么我会单独列出来呢？

现今的软件项目，尤其是互联网项目，生命周期通常以“月”甚至是以“周”、“小时”为单位，一个测试工程师需要接触各种类型的测试项目，而不再像早年，可以在很长一段时间内只从事一个产品或者相关产品的测试了，所以快速学习能力对测试工程师来说，就是至关重要了，否则就容易被淘汰。

快速学习能力，乍一看是比较难培养的，但其实也有一些小窍门。

比如，当你学习一个新的开源工具时，建议你直接看官方文档：一来，这里的内容是最新而且是最权威的；二来，可以避免网上信息质量的参差不齐。知识输入源头是单一，而且权威的话，你的学习曲线也必然会比较平滑。

另外，当学习新内容时，你一定要做到理解其原理，而不是只停留在表面的、简单的操作和使用，长期保持这种学习状态，可以在很大程度上提高逻辑思维和理解能力。这样，当你再面对其他新鲜事物时候，也会更容易理解，形成良性循环。

第四项核心竞争力，探索性测试思维

探索性测试是指，测试工程师在执行测试的过程中不断学习被测系统，同时结合基于自己经验的错误猜测和逻辑推理，整理和分析出更多的有针对性的测试关注点。

本质上，探索性测试思维是“测试用例设计能力”和“快速学习能力”有机结合的必然结果。优秀的探索性测试思维可以帮助你实现低成本的“精准测试”，精准测试最通俗的理解可以概括为针对开发代码的变更，目标明确并且有针对性地对变更点以及变更关联点做测试，这也是目前敏捷测试主推的测试实践之一。

第五项核心竞争力，缺陷分析能力

缺陷分析能力，通常包含三个层面的含义：

对于已经发现的缺陷，结合发生错误的上下文以及后台日志，可以预测或者定位缺陷的发生原因，甚至可以明确指出具体出错的代码行，由此可以大幅缩短缺陷的修复周期，并提高开发工程师对于测试工程师的认可以及信任度；

根据已经发现的缺陷，结合探索性测试思维，推断同类缺陷存在的可能性，并由此找出所有相关的潜在缺陷；

可以对一段时间内所发生的缺陷类型和趋势进行合理分析，由点到面预估整体质量的健康状态，并能够对高频缺陷类型提供系统性的发现和预防措施，并以此来调整后续的测试策略。

这三个层面是依次递进的关系，越往后越能体现出测试工程师的核心竞争力。

第六项核心竞争力，自动化测试技术

掌握自动化测试技术，可以把你从大量的重复性手工劳动中解放出来，这样你可以把更多的时间花在更多类型的测试上。

一方面，自动化测试技术本身不绑定被测对象，比如说你掌握了 GUI 的自动化测试技术，那么你就可以基于这个技术去做任何 GUI 系统的界面功能测试了。

另一方面，自动化测试技术需要测试工程师具备一定的写代码的能力，这通常与测试工程师职业发展的诉求不谋而合，所以你会看到很多测试工程师非常热衷做自动化测试。

但是切记，自动化测试的核心价值还是“测试”本身，“自动化”仅仅是手段，实际工作中千万不要本末倒置，把大量的精力放在“自动化”上，一味追求自动化而把本质的“测试”弱化了。

第七项核心竞争力，良好的沟通能力

测试工程师在软件项目中作用，有点像“润滑剂”：

一方面，你需要对接产品经理和项目经理，以确保需求的正确实现和项目整体质量的达标；

另一方面，你还要和开发人员不断地沟通、协调，确保缺陷的及时修复与验证。

所以，测试工程师的沟通能力会直接影响事务开展的效率。良好清晰的沟通能力，是一个技术优秀的测试工程师能否获得更大发展的“敲门砖”，也是资深测试工程师或者测试主管的核心竞争力。

测试开发工程师的核心竞争力

接下来，我再带你一起看看测试开发工程师的核心竞争力。

首先既然是测试开发工程师，那么代码开发能力是最基本的要求。可以说，一个合格的测试开发工程师一定可以成为一个合格的开发工程师，但是一个合格的开发工程师不一定可以成为合格的测试开发工程师。这也就是案例二中的候选人没有通过面试的原因。

第一项核心竞争力，测试系统需求分析能力

除了代码开发能力，测试开发工程师更要具备测试系统需求分析的能力。你要能够站在测试架构师的高度，识别出测试基础架构的需求和提高效率的应用场景。从这个角度说，你更像个产品经理，只不过你这个产品是为了软件测试服务的。

第二项核心竞争力，更宽广的知识体系

测试开发工程师需要具备非常宽广的知识体系，你不仅需要和传统的测试开发工程师打交道，因为他们是你构建的测试工具或者平台的用户；而且还要和 CI/CD、和运维工程师们有紧密的联系，因为你构建的测试工具或者平台，需要接入到 CI/CD 的流水线以及运维的监控系统中去。

除此之外，你还要了解更高级别的测试架构部署和生产架构部署、你还必须对开发采用的各种技术非常熟悉。可见，对于测试开发工程师的核心竞争力要求是非常高的，这也就是为什么现今市场上资深的测试开发工程师的价格会高于资深的开发工程师的原因。

总结

我把测试工程师按照工作内容，分为了功能测试工程师（即传统测试工程师）和测试开发工程师两类，分别给你分享了他们的核心竞争力。

对于功能测试工程师来说，其核心竞争力包括：测试策略设计能力、测试用例设计能力、快速学习能力、探索性测试思维、缺陷分析能力、自动化测试技术和良好的沟通能力这七大部分，你可以有针对性地提升自己某方面的能力，去获取更大发展空间的“敲门砖”。

而对于测试开发工程师来说，你需要具备优秀的测试系统需求分析能力和完备的知识体系，这样才能保证你设计的测试工作和平台，可以更好地满足提升测试效率的要求。

思考题

你有没有想过这样一个问题，你很少会听到开发工程师谈论自己的核心竞争力，往往都是测试工程师更关注这个问题，这是不是从某个侧面反映出测试工程师的核心竞争力不够清晰或者是随着互联网时代的到来而发生了很大变化，说说你的看法吧。

欢迎你给我留言。

10 | 软件测试工程师需要掌握的非测试知识有哪些？

我在上一篇文章中，跟你分享了测试工程师应该具备的核心竞争力，大多是测试专业知识方面的内容。但是，在专栏第一篇文章中，我提到了这样一个观点：一个优秀的测试工程师，必须具备宽广的知识面，才能设计出有的放矢的测试用例，保证整个软件产品的质量。

所以，今天我要分享的主题就是，除了测试专业知识外，你还要掌握哪些知识，才能一路披荆斩棘，成长为一名优秀的测试工程师，或者是测试架构师。

与开发工程师相比，你需要了解的技术种类要多得多，视野也要宽广很多，只是在每类技术的深度方面不如开发工程师。

你可以参照下面这个比喻，来理解开发工程师和测试工程师的对知识的要求：开发工程师通常是“深度遍历”，关注的是“点”；而测试工程师通常是“广度遍历”，关注的是“面”。

那么，测试工程师需要掌握的非测试知识主要有哪些呢？

如果你花时间静下心来仔细想一下，很可能会把自己吓一大跳，需要了解掌握的非测试知识实在是太多了，这简直就是一个 mini 版的系统架构师啊！

小到 Linux/Unix/Windows 操作系统的基础知识，Oracle/MySQL 等传统关系型数据库技术，NoSQL 非关系型数据库技术，中间件技术，Shell/Python 脚本开发，版本管理工具与策略，CI/CD 流水线设计，F5 负载均衡技术，Fiddler/Wireshark/Tcpdump 等抓包工具，浏览器 Developer Tool 等；

大到网站架构设计，容器技术，微服务架构，服务网格（Service Mesh），DevOps，云计算，大数据，人工智能和区块链技术等。

可以说，测试工程师需要掌握的这些技术，几乎涵盖了当今主流软件技术的方方面面。当然，你也不可能一口气吃成胖子，所以我就挑选了几个我认为比较重要，又符合当前技术趋势的关键知识点，和你分享。

希望我的分享，可以帮助你在面对新的技术趋势时，站在更高的高度，更好地把握测试工作的内涵和外延。

网站架构的核心知识

现如今，互联网产品已经占据了软件行业的大半壁以江山。作为测试工程师，你很多时候都在和互联网产品，尤其是网站类应用产品的测试打交道。

这时，如果你想要做好互联网产品功能测试以外的其他测试，比如性能测试、稳定性测试、全链路压测、故障切换（Failover）测试、动态集群容量伸缩测试、服务降级测试和安全渗透测试等，就要掌握网站的架构知识。否则，面对这类测试时，你将束手无策。

比如，如果你不清楚 Memcached 这类分布式缓存集群的应用场景和基本原理，如果你不清楚缓存击穿、缓存雪崩、缓存预热、缓存集群扩容局限性等问题，你就设计不出针对缓存系统特有问题的测试用例；

再比如，如果你对网站的可伸缩性架构设计不了解，不清楚应用服务器的各种负载均衡实现的基本原理，不了解数据库的读写分离技术，你就无法完成诸如故障切换、动态集群容量伸缩、服务降级等相关的测试，同时对于性能测试和全链路压测过程中可能遇到的各种瓶颈，也会很难定位和调整。

这就有点像当年做传统软件产品测试时，我们必须了解软件的架构设计一样，现在被测对象成了互联网产品，我们就必须要了解网站架构。

所以，我强烈建议你要掌握网站架构的核心知识，你不需要像系统架构师那样能够熟练驾驭各种架构，并根据业务选型，但你至少需要理解架构相关的基本知识以及核心原理。

基于此，我在专栏的最后安排了一系列文章，包括了网站高性能架构设计、网站高可用架构设计、网站伸缩性架构设计和网站可扩展性架构设计，为你详细讲解互联网架构的核心知识，提升你的互联网产品测试能力。

容器技术

“容器”已不再是一个陌生词汇了，大多数人都在实际工作中或多或少地用到了容器技术。与传统的虚拟机相比，容器技术在轻量化程度、资源占用、运行效率等方面具有压倒性的优势。

除了那些专门做容器测试的测试工程师外，一般的测试工程师接触容器技术的机会也越来越多。

很多中大型互联网企业都在推行容器化开发与运维，开发人员递交给测试工程师的软件版本通常就是一个 Docker Image，直接在容器上进行测试。有些公司还会把测试用例和执行框架也打包成 Docker Image，配合版本管理机制，实现用容器测试容器。

对测试开发工程师来说，需要应用容器的场景就更多了。比如，目前主流的 Selenium Grid 就已经提供了官方 Docker 版本，可以直接以容器的方式建立测试执行环境，也可以很方便地在 Pivotal Cloud Foundry 和 Google Cloud Platform 等云计算平台上快速建立测试执行环境。

基于 Docker 的 Selenium Grid 大大减轻了批量虚拟机节点上 Web Driver、浏览器版本和守护者进程版本等升级维护的工作量。

测试开发工程师还可以通过 Docker Image 的形式，提供某些测试工具，而不是以传统的安装包或者 JAR 文件的形式，可以实现测试工具开箱即用。

可见，容器技术已经慢慢渗透到软件研发与运维的各个层面，作为新时代的测试开发工程师，你必须像熟练使用 VMware 一样，掌握 Docker 和 Kubernetes 的原理和使用方法。

那对于一个测试工程师来说，怎么才能快速具备容器相关知识，并上手涉及容器技术的互联网产品测试呢？

在这里，我还是要跟你强调选择学习资料时，一定要注意权威性，我给你的推荐依然是Docker 官网的教程，在这里你完全可以理清 Docker 概念以及具体使用方法，那再结合具体的实战，相信你必定收获颇丰。

云计算技术

一方面，很多企业，尤其是互联网企业都在尝试“上云”， 也就是逐渐把生产环境从原本的集中式数据中心模式转向私有云或者混合云模式。

前段时间，eBay 的一些产品线就对外宣布了和 Pivotal Cloud Foundry 的合作，会将部分产品线迁移到云端。显然，作为测试工程师，你必须理解服务在云端部署的技术细节才能更好的完成测试任务。

另一方面，测试基础服务作为提供测试服务的基础设施，比如测试执行环境服务（Test Execution Service）和测试数据准备服务（Test Data Service）等，也在逐渐走向云端。 比如，国外非常流行的 Sauce Labs，就是一个著名的测试执行环境公有云服务。

一些大型互联网企业，通常还会考虑建立自己的测试执行私有云。最典型的就是，基于 Appium + Selenium Grid，搭建移动终端设备的测试执行私有云。

所以，除了专门进行云计算平台测试的工程师，必须要掌握云计算的知识外，其他互联网产品的测试工程师，也要能够理解并掌握基本的云计算知识和技术。

在我看来，对于云计算的学习，你的侧重点应该是如何使用云提供的基础设施以及服务。我建议的高效学习方法是，参考你所采用的云方案的官方文档，再结合实际案例进行试用，学习效果会更好。

你可以尝试用云服务去部署自己的应用，同时还可以结合云平台提供的各类服务（配置服务，数据库服务等）和你的应用做集成。另外，我还建议你尝试用云平台建立自己的小应用集群，体验集群规模的动态收缩与扩展。你还可以尝试在云平台上直接使用 Docker 部署发布你的服务。

更进一步，你可以尝试在云端建立自己的 Selenium Gird 集群，现在 Selenium Gird 已经发布了对应的 Docker 版本镜像，你可以非常方便地在云平台上搭建自己的 Selenium Grid。

不要以为这会有多复杂，理解了 Docker 的基本概念以及对应云平台的使用方法，你就可以在短时间内快速搭建起这样的 Selenium 集群。

相信以上这些基本的应用场景，都将更好地帮助你理解云平台的核心功能以及使用场景，从而帮你完成对应产品的测试。

DevOps 思维

DevOps 强调的是，开发、测试和运维等组织团队之间，通过高效自动化工具的协作和沟通，来完成软件的全生命周期管理，从而实现更频繁地持续交付高质量的软件，其根本目的是要提升业务的交付能力。

DevOps 的具体表现形式可以是工具、方法和流水线，但其更深层次的内涵还是在思想方法，以敏捷和精益为核心，通过发现问题，以系统性的方法或者工具来解决问题，从而实现持续改进。

因此，测试工程师也必须深入理解 DevOps 思想的核心和精髓，才能在自动化测试和测试工具平台的实现上做出最契合的设计。无论是测试工程师，还是测试开发工程师，都会成为 DevOps 实践成功落地的重要推动力。

要想真正学习和掌握 DevOps，并不是简单地学习几款工具的使用，更重要的是需要有 DevOps 思维，能够将各个工具有机结合，提供高效的 CI/CD 流水线。

对于 DevOps，我建议的学习路径是，你可以从深入掌握 Jenkins 之类的工具开始，到熟练应用和组合各种 plugin 来完成灵活高效的流水线搭建，之后再将更多的工具逐渐集成到流水线中以完成更多的任务。

相信通过这样的学习，当你再面对相关的测试工作时，必然可以轻松应对。

前端开发技术

前端开发技术的发展突飞猛进，新的框架与技术层出不穷，Vue.js，Angular 和 React 等让人应接不暇。并且，还有很多在此类框架基础上开发的组件库可以直接使用，比如 AntD，大大降低了前端开发的难度和时间成本。

但是，前端开发技术的发展和测试又有什么关系呢？

从测试工程师的角度来讲，如果你能够掌握前端开发技术，也就意味着你可以更高效地做前端的测试，更容易发现潜在缺陷。同时，你还可以自己构建测试页面，来完成各类前端组件的精细化测试，大大提高测试覆盖率和效率。

从测试开发工程师的角度来讲，很多测试平台和工具都需要 UI 界面，比如很多公司内部构建的测试数据服务和测试执行服务，如果你能熟练掌握基本的前端开发技术，那你就可以很方便、高效地构建测试平台和工具的 UI。

关于前端技术的学习路径，通常你首先需要掌握最基本的 JavaScript、CSS、JQuery 和 HTML5 等知识，然后再去学习一些主流的前端开发框架，比如 Angular.js、Backbone.js 等。当然现在的 Node.js 的生态圈非常发达，你如果能够掌握 Node.js，那么很多东西实现起来都可以得心应手。

我个人推荐从网上下载一些样例代码进行学习，同时学习使用脚手架从无到有去建立自己的前端应用。

总结

为了应对技术发展趋势，做好软件产品的测试工作，软件测试工程师需要掌握非常多的非测试专业知识，包括：网站架构、容器技术、云计算技术、DevOps 思维，以及前端开发技术的核心知识以及实践。

对于这类新技术的学习，我强烈推荐你直接阅读官方网站的文档以及代码示例。这种方式，可以让你少走弯路，同时保证所学内容是最新的。

当然，我跟你分享的这些非测试专业知识，只是众多技术的冰山一角，你在实际的测试工作中也会遇到更多的技术，希望你可以举一反三，不断扩充自己的知识面，向着一个优秀测试工程师、架构师努力！

思考题

你所在的测试领域，还有哪些非测试知识是必须掌握的，这些知识可以从哪些方面帮到你呢？

欢迎你给我留言。

11 | 互联网产品的测试策略应该如何设计？

在上一篇文章中，我跟你分享了做好互联网产品测试你要具备的非测试知识，那么现在我就来跟你聊聊应该如何设计互联网产品的测试策略。

在我开始今天的话题之前，请你先思考一下为什么我会把互联网产品的测试策略单独拿出来讨论，互联网产品的测试策略和传统软件产品的测试策略到底有哪些不同？

研发流程的不同决定了测试策略的不同

如果直接回答互联网产品和传统软件产品的测试策略有何不同，你会有些摸不着头脑，那么按照我一直在强调的知其然知其所以然的原则，你可以先去总结这两类产品的研发本身最大的不同是什么？

那就是，互联网产品的“快”。

我在专栏前面的文章中，已经提到了互联网产品的上线周期通常是以“天”甚至是以“小时”为单位，而传统软件产品的周期多以“月”，甚至以“年”为单位。

发布周期的巨大差异决定了，传统软件产品的测试策略必然不适用于互联网产品的测试，二者的测试策略必然在测试执行时间和测试执行环境上有巨大差异。

比如，对于功能自动化测试用例，执行一轮全回归测试需要 12 小时，对传统软件来说这根本不是问题，因为发布周期很长，留给测试的时间也会很充裕。

不要说全回归测试执行时间需要 12 小时，哪怕是需要几天几夜也没有任何问题，就像我以前在思科（Cisco）做传统软件测试时，一轮完整的全回归测试的 GUI 测试用例数接近 3000 个，API 测试用例数更是接近 25000 个，跑完全部用例需要将近 60 小时。

但对互联网产品来说，通常 24 小时就会有一到两次的发布，发布流程通常包含了代码静态扫描、单元测试、编译、打包、上传、下载、部署和测试的全流程。显然留给测试执行的时间就非常有限，传统软件动辄十几个小时的测试执行时间，在互联网产品的测试上，根本行不通。

通常情况下，互联网产品要求全回归测试的执行时间不能超过 4 小时。

那么，如何在保证测试质量和测试覆盖率的前提下，有效缩短测试执行时间呢？

首先，你可以引入测试的并发执行机制，用包含大量测试执行节点的测试执行集群来并发执行测试用例。
测试执行集群，你可以简单理解为是一批专门用来并发执行测试用例的机器。常见的测试执行集群，由一个主节点（Master）和若干个子节点（Node）组成。其中，主节点用来分发测试用例到各个子节点，而各个子节点用来具体执行测试用例。
目前，很多互联网企业都建立了自己的测试执行集群。

其次，你必须从测试策略上找到突破口，这也是我今天跟你分享的主题。
接下来，我会先简单为你介绍一下传统软件产品的测试策略设计，然后再给你分享互联网产品的测试策略，这样可以通过对传统软件产品测试策略的回顾，加深你对互联网产品测试策略的认识。

传统软件产品的测试策略设计

传统软件产品的测试策略，通常采用如图 1 所示的金字塔模型。该金字塔模型是迈克 · 科恩（Mike Cohn）提出的，在很长一段时间内都被认为是测试策略设计的最佳实践。

图 1 传统软件产品的金字塔测试策略

第一，单元测试

金字塔最底部是单元测试，属于白盒测试的范畴，通常由开发工程师自己完成，由于越早发现缺陷其修复成本越低，所以传统软件产品的测试策略提倡对单元测试的高投入，单元测试这一层通常都会做得比较“厚”。

另外，传统软件产品，生命周期都比较长，通常会有多个版本持续发布，为了在后期的版本升级过程中能够尽早发现并快速定位问题，每次 build 过程中都会多次反复执行单元测试，这也从另一个角度反映出单元测试的重要性。

第二，API 测试

金字塔中间部分是 API 测试，主要针对的是各模块暴露的接口，通常采用灰盒测试方法。灰盒测试方法是介于白盒测试和黑盒测试之间的一种测试技术，其核心思想是利用测试执行的代码覆盖率来指导测试用例的设计。

以 API 接口测试为例，首先以黑盒方式设计如何调用 API 的测试用例，同时在测试执行过程中统计代码覆盖率，然后根据代码覆盖率情况来补充更多、更有针对性的测试用例。

总体来看，API 测试用例的数量会少于单元测试，但多于上层的 GUI 测试。

第三，GUI 测试

金字塔最上层的是 GUI 测试，也称为端到端（E2E，End-to-end）测试，是最接近软件真实用户使用行为的测试类型。通常是模拟真实用户使用软件的行为，即模拟用户在软件界面上的各种操作，并验证这些操作对应的结果是否正确。

GUI 测试的优点是，能够实际模拟真实用户的行为，直接验证软件的商业价值；缺点是执行的代价比较大，就算是采用 GUI 自动化测试技术，用例的维护和执行代价依然很大。所以，要尽可能地避免对 GUI 测试的过度依赖。

另外，GUI 测试的稳定性问题，是长期以来阻碍 GUI 测试发展的重要原因。即使你采用了很多诸如 retry 机制以及异常场景恢复机制等方式，GUI 测试的随机失败率依旧高居不下。

互联网产品的测试策略设计

对于互联网产品来说，迈克的金字塔模型已经不再适用，我会通过 GUI 测试、API 测试、单元测试这三个方面，来跟你聊聊互联网产品的测试策略有哪些变化，应该如何设计。

第一，GUI 测试

互联网产品的上线周期，决定了 GUI 测试不可能大范围开展。

互联网产品的迭代周期，决定了留给开发 GUI 自动化测试用例的时间非常有限；

互联网产品客户端界面的频繁变化，决定了开展 GUI 自动化测试的效率会非常低，这也是最糟糕的。
因为敏捷模式下的快速反馈，在下一个迭代（sprint）可能就需要根据反馈来做修改和调整客户端界面，那么刚开发完，甚至是还没开发完的 GUI 自动化测试用例就要跟着一起修改。
这种频繁地修改，对开发 GUI 自动化测试是非常不利的。因为，刚开发完的自动化用例只跑了一次，甚至是一次还没来得及跑就需要更新了，导致 GUI 自动化测试还不如手工测试的效率高。

由此，互联网产品的 GUI 测试通常采用“手工为主，自动化为辅”的测试策略，手工测试往往利用探索性测试思想，针对新开发或者新修改的界面功能进行测试，而自动化测试的关注点主要放在相对稳定且核心业务的基本功能验证上。所以，GUI 的自动化测试往往只覆盖最核心且直接影响主营业务流程的 E2E 场景。

另外，从 GUI 测试用例的数量来看，传统软件的 GUI 测试属于重量级的，动不动就有上千个用例，因为传统软件的测试周期很长，测试用例可以轮流排队慢慢执行，时间长点也没关系。

而互联网产品要求 GUI 测试是轻量级的，你见过或者听过有哪个互联网产品设计了上千个 GUI 测试用例吗？互联网产品的上线周期，直接决定了不允许你去执行大量的用例。

第二，API 测试

你现在可能要问，既然互联网产品不适宜做重量级的 GUI 测试，那么怎样才能保证其质量呢？

其实，对于互联网产品来说，把测试重点放在 API 测试上，才是最明智的选择。为什么呢？我给你总结了以下五条原因。

API 测试用例的开发与调试效率比 GUI 测试要高得多，而且测试用例的代码实现比较规范，通常就是准备测试数据，发起 request，验证 response 这几个标准步骤。

API 测试用例的执行稳定性远远高于 GUI 测试。 GUI 测试执行的稳定性始终是难题，即使你采用了很多技术手段（这些具体的技术手段，我会在讲解 GUI 测试时再详细展开），它也无法做到 100% 的稳定。
而 API 测试天生就没有执行稳定性的问题，因为测试执行过程不依赖于任何界面上的操作，而是直接调用后端 API，且调用过程比较标准。

单个 API 测试用例的执行时间往往要比 GUI 测试短很多。当有大量 API 测试需要执行时，API 测试可以很方便地以并发的方式执行，所以可以在短时间内完成大批量 API 测试用例的执行。

现在很多互联网产品采用了微服务架构，而对微服务的测试，本质上就是对不同的 Web Service 的测试，也就是 API 测试。
在微服务架构下，客户端应用的实现都是基于对后端微服务的调用，如果做好了每个后端服务的测试，你就会对应用的整体质量有充分的信心。所以，互联网产品的 API 测试非常重要。

API 接口的改动一般比较少，即使有改动，绝大多数情况下也需要保证后向兼容性（Backward Compatibility）。所谓后向兼容性，最基本的要求就是保证原本的 API 调用方式维持不变。
显然，如果调用方式没有发生变化，那么原本的 API 测试用例也就不需要做大的改动，这样用例的可重用性就很高，进而可以保证较高的投入产出比（ROI）。

可见，互联网产品的这些特性决定了，API 测试可以实现良好的投入产出比，因此应该成为互联网产品的测试重点。这也就是为什么互联网产品的测试策略更像是个菱形结构的原因。

如图 2 所示就是这个菱形的测试策略，遵循“重量级 API 测试，轻量级 GUI 测试，轻量级单元测试”的原则。

图 2 互联网产品的菱形测试策略

第三，单元测试

了解了“重量级 API 测试”和“轻量级 GUI 测试”，接下来，我就跟你说说为什么是“轻量级单元测试”。

从理论上讲，无论是传统软件产品还是互联网产品，单元测试都是从源头保证软件质量的重要手段，因此都非常重要。但现实是，互联网产品真正能全面开展单元测试，并严格控制代码覆盖率的企业还是凤毛麟角。

但凡存在的都会有其合理性，我认为最主要的原因还是在于互联网产品的“快”，快速实现功能，快速寻求用户反馈，快速试错，快速迭代更新。

在这样的模式下，互联网产品追求的是最快速的功能实现并上线，基本不会给你时间去做全面的单元测试。即使给你预留了单元测试的时间，频繁的迭代也会让单元测试处于不断重写的状态。因此，单元测试原本的价值，很难在实际操作层面得到体现。

那么，互联网产品真的可以不用做单元测试么？答案是否定的，只不是这里的单元测试策略要采用“分而治之”的思想。

互联网产品通常会分为应用层和后端服务，后端服务又可以进一步细分为应用服务和基础服务。

后端基础服务和一些公共应用服务相对稳定，而且对于系统全局来说是“牵一发而动全身”，所以后端服务很有必要开展全面的单元测试；而对于变动非常频繁的客户端应用和非公用的后端应用服务，一般很少会去做单元测试。

另外，对于一些核心算法和关键应用，比如银行网关接口，第三方支付集成接口等，也要做比较全面的单元测试。

总结来讲，互联网产品的全面单元测试只会应用在那些相对稳定和最核心的模块和服务上，而应用层或者上层业务服务很少会大规模开展单元测试。

总结

传统软件通常采用金字塔模型的测试策略，而现今的互联网产品往往采用菱形模型。菱形模型有以下四个关键点：

以中间层的 API 测试为重点做全面的测试。

轻量级的 GUI 测试，只覆盖最核心直接影响主营业务流程的 E2E 场景。

最上层的 GUI 测试通常利用探索式测试思维，以人工测试的方式发现尽可能多的潜在问题。

单元测试采用“分而治之”的思想，只对那些相对稳定并且核心的服务和模块开展全面的单元测试，而应用层或者上层业务只会做少量的单元测试。

思考题

你所在的公司或者产品线，采用的是什么测试策略？看完了本篇文章，你会如何评价你们公司的测试策略呢？有哪些好的地方，又有哪些地方需要改进？

欢迎你给我留言。

12 | 从0到1：你的第一个GUI自动化测试

在前面的测试基础知识系列文章中，我分享了测试相关的基础知识，从测试用例的设计，到测试覆盖率，再到测试计划的制定，这些都是我认为测试人要掌握的一些基本知识。

那么，接下来我将要带你走入 GUI 自动化测试的世界，和你一起聊聊 GUI 自动化测试的技术、原理和行业最佳实践。

作为该系列的第一篇文章，我直接以一个最简单的 GUI 自动化用例的开发为例，带你从 0 开始构建一个 Selenium 的 GUI 自动化测试用例。

先让你对 GUI 自动化测试有一个感性认识，然后以此为基础，我再来解释 Selenium 自动化测试实现的核心原理与机制，希望可以帮你由点到面建立起 GUI 测试的基础知识体系。

构建一个 Selenium 自动化测试用例示例

测试需求非常简单：访问百度主页，搜索某个关键词，并验证搜索结果页面的标题是“被搜索的关键词”+“_ 百度搜索”。

如果搜索的关键词是“极客时间”，那么搜索结果页面的标题就应该是“极客时间 _ 百度搜索”。

明白了测试需求后，我强烈建议你先用手工方式执行一遍测试，具体步骤是：

打开 Chrome 浏览器，输入百度的网址“www.baidu.com”；

在搜索输入框中输入关键词“极客时间”并按下回车键；

验证搜索结果页面的标题是否是“极客时间 _ 百度搜索”。

明确了 GUI 测试的具体步骤后，我们就可以用 Java 代码，基于 Selenium 实现这个测试用例了。

这里，我要用到 Chrome 浏览器，所以需要先下载 Chrome Driver 并将其放入环境变量。接下来，你可以用自己熟悉的方式建立一个空的 Maven 项目，然后在 POM 文件中加入 Selenium 2.0 的依赖，如图 1 所示。

图 1 在 POM 文件中加入 Selenium 2.0 的依赖

接着用 Java 创建一个 main 方法，并把如图 2 所示的代码复制到你的 main 方法中。

图 2 基于 Selenium 的自动化测试用例的样本代码

现在，你可以尝试运行这个 main 方法，看看会执行哪些操作。

这段代码会自动在你的电脑上打开 Chrome 浏览器；

在 URL 栏自动输入“www.baidu.com”；

百度主页打开后，在输入框自动输入“极客时间”并执行搜索；

返回搜索结果页面；

Chrome 浏览器自动退出。

以上这些步骤都是由自动化测试代码自动完成的。

如果你已经接触过 GUI 自动化测试，你可能习以为常了，感觉没什么神奇的。但如果你是第一次接触 GUI 自动化测试，是不是觉得还蛮有意思的。

现在，我来快速解读一下这些代码，你可以看看这些自动化步骤是怎么实现的，更具体的原理和内部机制我会在后面文章中详细展开。

第 11 行，WebDriver driver = new ChromeDriver()，先创建一个 Chrome Driver 的实例，也就是打开了 Chrome 浏览器，但实际上没这么简单，后台还做了些额外的 Web Service 绑定工作，具体后面会解释；

第 14 行，driver.navigate().to(s: “http://www.baidu.com”) 用刚才已经打开的 Chrome 浏览器访问百度主页；

第 18 行，WebElement search_input = driver.findElement(By.name(“wd”))，使用 driver 的 findElement 方法，并通过 name 属性定位到了搜索输入框，并将该搜索输入框命名为 search_input；

第 21 行，search_input.sendKeys(…charSequences:“极客时间”)，通过 WebElement 的 sendKeys 方法向搜索输入框 search_input 输入了字符串“极客时间”；

第 24 行，search_input.submit()，递交了搜索请求；

第 27 行，Thread.sleep(millis:3000)，强行等待了固定的 3 秒时间；

第 30 行，Assert.assertEquals(expected:“极客时间 _ 百度搜索”,driver.getTitle())，通过 junit 的 assertEquals 比较了浏览器的标题与预计结果，其中页面标题通过 driver 的 getTitle 方法得到，如果标题与预计结果一致，测试通过，否则测试失败；

第 33 行，driver.quit()，显式关闭了 Chrome 浏览器。

现在，你对 main 方法中的代码，已经比较清楚了。但是，你知道 Selenium 内部是如何实现 Web 自动化操作的吗？这就要从 Selenium 的历史版本和基本原理开始讲起了。

Selenium 的实现原理

首先，你要明确刚才建立的测试用例是基于 Selenium 2.0，也就是 Selenium + WebDriver 的方案。

其次，你需要知道，对 Selenium 而言，V1.0 和 V2.0 版本的技术方案是截然不同的，V1.0 的核心是 Selenium RC，而 V2.0 的核心是 WebDriver，可以说这完全是两个东西。

最后，Selenium 3.0 也已经发布一段时间了，V3.0 相比 V2.0 并没有本质上的变化，主要是增加了对 MacOS 的 Safari 和 Windows 的 Edge 的支持，并彻底删除了对 Selenium RC 的支持。

所以接下来，我会针对 V1.0 和 V2.0 来解释 Selenium 实现 Web 自动化的原理。

第一，Selenium 1.0 的工作原理

Selenium 1.0，又称 Selenium RC，其中 RC 是 Remote Control 的缩写。Selenium RC 利用的原理是：JavaScript 代码可以很方便地获取页面上的任何元素并执行各种操作。

但是因为"同源政策（Same-origin policy）"（只有来自相同域名、端口和协议的 JavaScript 代码才能被浏览器执行），所以要想在测试用例运行中的浏览器中，注入 JavaScript 代码从而实现自动化的 Web 操作，Selenium RC 就必须“欺骗”被测站点，让它误以为被注入的代码是同源的。

那如何实现“欺骗”呢？这其实就是引入 Selenium RC Server 的根本原因，其中的 Http Proxy 模块就是用来“欺骗”浏览器的。

除了 Selenium RC Server，Selenium RC 方案的另一大部分就是，Client Libraries。它们的具体关系如图 3 所示。

图 3 Selenium RC 的基本模块

Selenium RC Server，主要包括 Selenium Core，Http Proxy 和 Launcher 三部分：

Selenium Core，是被注入到浏览器页面中的 JavaScript 函数集合，用来实现界面元素的识别和操作；

Http Proxy，作为代理服务器修改 JavaScript 的源，以达到“欺骗”被测站点的目的；

Launcher，用来在启动测试浏览器时完成 Selenium Core 的注入和浏览器代理的设置。

Client Libraries，是测试用例代码向 Selenium RC Server 发送 Http 请求的接口，支持多种语言，包括 Java、C# 和 Ruby 等。

为了帮你更好地理解 Selenium RC 的基本原理，我从 Selenium 的官方网站截取了以下执行流程图，并把具体的 7 个步骤做了如下翻译。

图 4 Selenium RC 的执行流程

测试用例通过基于不同语言的 Client Libraries 向 Selenium RC Server 发送 Http 请求，要求与其建立连接。

连接建立后，Selenium RC Server 的 Launcher 就会启动浏览器或者重用之前已经打开的浏览器，把 Selenium Core（JavaScript 函数的集合）加载到浏览器页面当中，并同时把浏览器的代理设置为 Http Proxy。

测试用例通过 Client Libraries 向 Selenium RC Server 发送 Http 请求，Selenium RC Server 解析请求，然后通过 Http Proxy 发送 JavaScript 命令通知 Selenium Core 执行浏览器上控件的具体操作。

Selenium Core 接收到指令后，执行操作。

如果浏览器收到新的页面请求信息，则会发送 Http 请求来请求新的 Web 页面。由于 Launcher 在启动浏览器时把 Http Proxy 设置成为了浏览器的代理，所以 Selenium RC Server 会接收到所有由它启动的浏览器发送的请求。

Selenium RC Server 接收到浏览器发送的 Http 请求后，重组 Http 请求以规避“同源策略”，然后获取对应的 Web 页面。

Http Proxy 把接收的 Web 页面返回给浏览器，浏览器对接收的页面进行渲染。

第二，Selenium 2.0 的工作原理

接下来，我们回到上面那个百度搜索的测试用例，这个测试用例用的就是 Selenium 2.0。Selenium 2.0，又称 Selenium WebDriver，它利用的原理是：使用浏览器原生的 WebDriver 实现页面操作。它的实现方式完全不同于 Selenium 1.0。

Selenium WebDriver 是典型的 Server-Client 模式，Server 端就是 Remote Server。以下是 Selenium 2.0 工作原理的解析。

图 5 Selenium WebDriver 的执行流程

当使用 Selenium2.0 启动浏览器 Web Browser 时，后台会同时启动基于 WebDriver Wire 协议的 Web Service 作为 Selenium 的 Remote Server，并将其与浏览器绑定。绑定完成后，Remote Server 就开始监听 Client 端的操作请求。

执行测试时，测试用例会作为 Client 端，将需要执行的页面操作请求以 Http Request 的方式发送给 Remote Server。该 HTTP Request 的 body，是以 WebDriver Wire 协议规定的 JSON 格式来描述需要浏览器执行的具体操作。

Remote Server 接收到请求后，会对请求进行解析，并将解析结果发给 WebDriver，由 WebDriver 实际执行浏览器的操作。

WebDriver 可以看做是直接操作浏览器的原生组件（Native Component），所以搭建测试环境时，通常都需要先下载浏览器对应的 WebDriver。

总结

首先，我基于 Selenium 2.0，带你从 0 到 1 建立了一个最简单直接的 GUI 自动化测试用例。这个用例的实现很简单，但是只有真正理解了 Selenium 工具的原理，你才能真正用好它。

所以，我又分享了 Selenium 1.0 和 Selenium 2.0 的内部实现机制和原理：Selenium 1.0 的核心是，基于 JavaScript 代码注入；而 Selenium 2.0 的核心是，运用了浏览器原生支持的 WebDriver。

思考题

除了 Selenium，业内还有很多常用的 GUI 自动化测试框架，比如 UFT（以前的 QTP）、RFT、Nightwatch 等，你在平时的工作中接触过哪些 GUI 自动化测试框架？你知道它们的内部实现原理吗？

欢迎你给我留言。

13 | 效率为王：脚本与数据的解耦 + Page Object模型

在上一篇文章中，我用 Selenium 2.0 实现了我们的第一个 GUI 自动化测试用例，在你感觉神奇的同时，是否也隐隐感到一丝丝的担忧呢？比如，测试脚本中既有测试数据又有测试操作，所有操作都集中在一个脚本中等等。

那么，今天我就通过介绍 GUI 测试中两个非常重要的概念：测试脚本和数据的解耦，以及页面对象（Page Object）模型，带你看看如何优化这个测试用例。

测试脚本和数据的解耦

我在前面的文章中，和你分享过 GUI 自动化测试适用的场景，它尤其适用于需要回归测试页面功能的场景。那么，你现在已经掌握了一些基本的 GUI 自动化测试用例的实现方法，是不是正摩拳擦掌准备批量开发 GUI 自动化脚本，把自己从简单、重复的 GUI 界面操作中解放出来呢？

但是，你很快就会发现，如果在测试脚本中硬编码（hardcode）测试数据的话，测试脚本灵活性会非常低。而且，对于那些具有相同页面操作，而只是测试输入数据不同的用例来说，就会存在大量重复的代码。

举个最简单的例子，上一篇文章中实现的百度搜索的测试用例，当时用例中搜索的关键词是“极客时间”，假设我们还需要测试搜索关键词是“极客邦”和“InfoQ”的场景，如果不做任何处理，那我们就可能需要将之前的代码复制 3 份，每份代码的主体完全一致，只是其中的搜索关键词和断言（Assert）的预期结果不同。

显然，这样的做法是低效的。

更糟糕的是，界面有任何的变更需要修改自动化脚本时，你之前复制出来的三个脚本都需要做相应的修改。比如，搜索输入框的名字发生了变化，你就需要修改所有脚本中 findElement 方法的 by.name 属性。

而这里只有三个脚本还好，如果有 30 个或者更多的脚本呢，你会发现脚本的维护成本实在是太高了。那么，这种情况应该怎么处理呢？

相信你现在已经想到了，把测试数据和测试脚本分离。也就是说测试脚本只有一份，其中需要输入数据的地方会用变量来代替，然后把测试输入数据单独放在一个文件中。这个存放测试输入数据的文件，通常是表格的形式，也就是最常见的 CSV 文件。

然后，在测试脚本中通过 data provider 去 CSV 文件中读取一行数据，赋值给相应的变量，执行测试用例。接着再去 CSV 文件中读取下一行数据，读取完所有的数据后，测试结束。CSV 文件中有几行数据，测试用例就会被执行几次。具体流程如图 1 所示。

图 1 数据驱动测试的基本概念

这也就是典型的数据驱动（Data-driven）测试了。

数据驱动很好地解决了大量重复脚本的问题，实现了“测试脚本和数据的解耦”。 目前几乎所有成熟的自动化测试工具和框架，都支持数据驱动的测试，而且除了支持 CSV 这种最常见的数据源外，还支持 xls 文件、JSON 文件，YAML 文件，甚至还有直接以数据库中的表作为数据源的，比如 QTP 就支持以数据库中的表作为数据驱动的数据源。

数据驱动测试的数据文件中不仅可以包含测试输入数据，还可以包含测试验证结果数据，甚至可以包含测试逻辑分支的控制变量。 图 1 中的“Result_LoginSuccess_Flag”变量其实就是用户分支控制变量。

数据驱动测试的思想不仅适用于 GUI 测试，还可以用于 API 测试、接口测试、单元测试等。 所以，很多 API 测试工具（比如 SoapUI），以及单元测试框架都支持数据驱动测试，它们往往都是通过 Test Data Provider 模块将外部测试数据源逐条“喂”给测试脚本。

页面对象（Page Object）模型

为了让你了解“页面对象（Page Object）模型”这个概念的来龙去脉，并能够深入理解这个概念的核心思想，我会先从早期的 GUI 自动化测试开始讲起。

早期的 GUI 自动化测试脚本，无论是用开源的 Selenium 开发，还是用商用的 QTP（Quick Test Professional，现在已经改名为 Unified Functional Testing）开发，脚本通常是由一系列的页面控件的顺序操作组成的，如图 2 所示的伪代码展示了一个典型的早期 GUI 测试脚本的结构。

图 2 早期的 GUI 测试脚本伪代码示例

我先来简单介绍一下这个脚本实现的功能。

第 1-4 行，输入用户名和密码并点击“登录”按钮，登录完成后页面将跳转至新页面；

第 5 行，在新页面找到“图书”链接，然后点击链接跳转至图书的页面；

第 7-10 行，在图书搜索框输入需要查找的书名，点击“搜索”按钮，然后通过 assert 验证搜索结果；

第 11-12 行，用户登出。

看完这段伪代码，你是不是觉得脚本有点像操作级别的“流水账”，而且可读性也比较差，这主要体现在以下几个方面：

脚本逻辑层次不够清晰，属于 All-in-one 的风格，既有页面元素的定位查找，又有对元素的操作。

脚本的可读性差。 为了方便你理解，示例中的代码用了比较直观的 findElementByName，你可以很方便地从 name 的取值，比如“username”和“password”，猜出脚本所执行的操作。
但在实际代码中，很多元素的定位都会采用 Xpath、ID 等方法，此时你就很难从代码中直观看出到底脚本在操作哪个控件了。也就是说代码的可读性会更差，带来的直接后果就是后期脚本的维护难度增大。
有些公司自动化测试脚本的开发和维护是两拨人，脚本开发并调试完以后，开发人员就会把脚本移交给自动化测试执行团队使用并维护，这种情况下脚本的可读性就至关重要了。但即使是同一拨人维护，一段时间后，当时的开发人员也会遗忘某些甚至是大部分的开发步骤。

由于脚本的每一行都直接描述各个页面上的元素操作，你很难一眼看出脚本更高层的业务测试流程。 比如图 2 的业务测试流程其实就三大步：用户登录、搜索书籍和用户登出，但是通过阅读代码很难一下看出来。

通用步骤会在大量测试脚本中重复出现。 脚本中的某些操作集合在业务上是属于通用步骤，比如上面伪代码的第 1-4 行完成的是用户登录操作，第 11-12 行完成的是用户的登出操作。

这些通用的操作，会在其他测试用例的脚本中被多次重复。无论操作发生变动，还是页面控件的定位发生变化时，都需要同时修改大量的脚本。

其实，我上面说到的这四点正是早期 GUI 自动化测试的主要问题，这也是我一直说“开发几个 GUI 自动化测试玩玩会觉得很高效，但是当你开发成百上千个 GUI 自动化测试的时候，你会很痛苦”的本质含义。

那怎么解决这个问题呢？你可能已经想到了软件设计中模块化设计的思想。

没错，就是利用模块化思想，把一些通用的操作集合打包成一个个名字有意义的函数，然后 GUI 自动化脚本直接去调用这些操作函数来构成整个测试用例，这样 GUI 自动化测试脚本就从原本的“流水账”过渡到了“可重用脚本片段”。

如图 3 所示，就是利用了模块化思想的伪代码。

图 3 基于模块化的 GUI 测试用例伪代码示例

第 1-6 行就是测试用例，非常简单直接，一眼就可以看出测试用例具体在执行什么操作，而各个操作函数的具体内部实现还是之前那些“流水账”。当然这里对于测试输入数据完全可以采用测试驱动方法，这里为了直观我就直接硬编码了测试示例数据。

实际工程应用中，第 1-6 行的测试用例和第 8-30 行的操作函数通常不会放在一个文件中，因为操作函数往往会被很多测试用例共享。这种模块化的设计思想，带来的好处包括：

解决了脚本可读性差的问题，脚本的逻辑层次也更清晰了；

解决了通用步骤会在大量测试脚本中重复出现的问题， 现在操作函数可以被多个测试用例共享，当某个步骤的操作或者界面控件发生变化时，只要一次性修改相关的操作函数就可以了，而不需要去每个测试用例中逐个修改。

但是，这样的设计并没有完全解决早期 GUI 自动化测试的主要问题，比如每个操作函数内部的脚本可读性问题依然存在，而且还引入了新的问题，即如何把控操作函数的粒度，以及如何衔接两个操作函数之间的页面。

关于这两个新引入的问题，我会在后面的文章中为你详细阐述。我先来跟你聊聊，怎么解决早期 GUI 自动化测试的“可读性差、难以维护”问题。

现在，操作函数的内部实现还只是停留在“既有页面元素的定位查找，又有对元素的操作”的阶段，当业务操作本身比较复杂或者需要跨多个页面时，“可读性差、难以维护”的问题就会暴露得更加明显了。

那么，有什么更好的办法来解决这个问题吗？答案就是，我要分享的 GUI 自动化测试的第二个概念：页面对象（Page Object）模型。

页面对象模型的核心理念是，以页面（Web Page 或者 Native App Page）为单位来封装页面上的控件以及控件的部分操作。而测试用例，更确切地说是操作函数，基于页面封装对象来完成具体的界面操作，最典型的模式是“XXXPage.YYYComponent.ZZZOperation”。

基于这个思想，上述用例的伪代码可以进化成如图 4 所示的结构。这里，我只给出了 login 函数的伪代码，建议你按照这种思路，自己去实现一下 search 和 logout 的代码，这样可以帮你更好的体会页面对象模型带来的变化。

图 4 基于页面对象模型的伪代码示例

通过这样的代码结构，你可以清楚地看到是在什么页面执行什么操作，代码的可读性以及可维护性大幅度提高，也可以更容易地将具体的测试步骤转换成测试脚本。

总结

今天我给你讲了什么是数据驱动的测试，让你明白了“测试脚本和数据解耦”的实现方式以及应用场景。接着从 GUI 自动化测试历史发展演变的角度引出了 GUI 测试中的“页面对象模型”的概念。

“测试脚本和数据解耦”的本质是实现了数据驱动的测试，让操作相同但是数据不同的测试可以通过同一套自动化测试脚本来实现，只是在每次测试执行时提供不同的测试输入数据。

“页面对象模型”的核心理念是，以页面为单位来封装页面上的控件以及控件的部分操作。而测试用例使用页面对象来完成具体的界面操作。

希望这篇文章，可以让你更清楚地认识 GUI 自动化测试用例的逻辑以及结构。同时，你可能已经发现，这篇文章的内容并不是局限在某个 GUI 自动化测试框架上，你可以把这些设计思想灵活地运用其他 GUI 自动化测试项目中，这也是我希望达到的“授人以鱼，不如授人以渔”。

思考题

我在文中有这样一段描述：页面对象模型的核心理念是，以页面为单位来封装页面上的控件以及控件的部分操作。但是，现在业界对“是否应该在页面对象模型中封装控件的操作”一直有不同的看法。

有些观点认为，可以在页面对象模型中封装页面控件的操作；而有些观点则认为，页面对象模型只封装控件，而操作应该再做一层额外的封装。

你更认同哪种观点呢，说说你的理由吧。

欢迎你给我留言。

14 | 更接近业务的抽象：让自动化测试脚本更好地描述业务

在上一篇文章中，我介绍了 GUI 自动化测试中的两个主要的概念“脚本与数据的解耦 ”以及“ 页面对象模型”。在引入“操作函数”封装时，我提到操作函数在改善测试脚本可读性问题的同时，也引入了两个新的问题，即： 如何把控操作函数的粒度，以及如何衔接两个操作函数之间的页面。

现在，我就以这两个问题作为引子，为你介绍 GUI 自动化测试中“业务流程（business flow）”的概念、核心思想以及应用场景。

如何把控操作函数的粒度？

操作函数的粒度是指，一个操作函数到底应该包含多少操作步骤才是最合适的。

如果粒度太大，就会降低操作函数的可重用性。极端的例子就是，前面文章中涉及的百度搜索的案例，把“登录”“搜索”“登出”的操作作为一个操作函数。

如果粒度太小，也就失去了操作函数封装的意义。极端的例子就是，把每一个步骤都作为一个操作函数。

更糟糕的是，在企业实际自动化测试开发中，每个测试工程师对操作函数的粒度理解也不完全相同，很有可能出现同一个项目中脚本粒度差异过大，以及某些操作函数的可重用性低的问题。

那么，操作函数的粒度到底应该如何控制呢？其实这个问题，在很大程度上取决于项目的实际情况，以及测试用例步骤的设计，并没有一个放之四海而皆准的绝对标准。

但是，脚本粒度的控制还是有设计依据可以遵循的，即往往以完成一个业务流程（business flow）为主线，抽象出其中的“高内聚低耦合”的操作步骤集合，操作函数就由这些操作步骤集合构成。

比如，对于“用户注册”这个业务流程，其中的“信用卡绑定”操作就会涉及多个操作步骤，而这些操作在逻辑上又是相对独立的，所以就可以包装成一个操作函数。也就是说，业务流程会依次调用各个操作函数，来完成具体的业务操作。

如何衔接两个操作函数之间的页面？

完成一个业务流程操作，往往会需要依次调用多个操作函数，但是操作函数和操作函数之间会有页面衔接的问题，即前序操作函数完成后的最后一个页面，必须是后续操作函数的第一个页面。

如果连续的两个操作函数之间无法用页面衔接，那就需要在两个操作函数之间加入额外的页面跳转代码，或者是在操作函数内部加入特定的页面跳转代码。

业务流程抽象

在解决如何把控操作函数的粒度，以及如何衔接两个操作函数之间的页面这两个问题的过程中，我引入了业务流程的概念。那么，接下来我就跟你详细说说什么是业务流程。

业务流程抽象是，基于操作函数的更接近于实际业务的更高层次的抽象方式。基于业务流程抽象实现的测试用例往往灵活性会非常好，你可以很方便地组装出各种测试用例。

这个概念有点拗口，难以理解。但是，没关系，我举个例子，你就豁然开朗了。

假设，某个具体的业务流程是：已注册的用户登录电商平台购买指定的书籍。那么，基于业务流程抽象的测试用例伪代码，如图 1 所示。

图 1 基于业务流程抽象的测试用例伪代码

这段伪代码的信息量很大，但是理解了这段代码的设计思想，你也就掌握了业务流程抽象的精髓。

首先，从整体结构上看，段伪代码顺序调用了 4 个业务流程， 依次是完成用户登录的 LoginFlow、完成书籍查询的 SearchBookFlow、完成书籍购买的 CheckoutBookFlow、完成用户登出的 LogoutFlow。

这 4 个业务流程都是作为独立的类封装的，可以被很方便的重用并灵活组合，类的内部实现通常是调用操作函数。而操作函数内部，则是基于页面对象模型完成具体的页面控件操作。

然后，对于每一个业务流程类，都会有相应的业务流程输入参数类与之一一对应。具体的步骤通常有这么几步：

初始化一个业务流程输入参数类的实例；

给这个实例赋值；

用这个输入参数实例来初始化业务流程类的实例；

执行这个业务流程实例。

执行业务流程实例的过程，其实就是调用操作函数来完成具体的页面对象操作的过程。

为了让你更好地理解业务流程抽象提供了哪些功能，接下来我会为你逐行解读这段伪代码。

伪代码的第 2-6 行，调用的是 LoginFlow，完成了用户登录的操作。

2: LoginFlowParameters loginFlowParameters = new LoginFlowParameters();

3: loginFlowParameters.setUserName("username");

4: loginFlowParameters.setPassword("password");

5: LoginFlow loginFlow = new LoginFlow(loginFlowParameters);

6: loginFlow.execute();

第 2 行，初始化了 LoginFlow 对应的 LoginFlowParameters 的实例。

第 3-4 行，通过 setUserName 和 setPassword 方法将用户名和密码传入该参数实例。

第 5 行，用这个已经赋值的参数实例来初始化 LoginFlow。

第 6 行，通过 execute 方法发起执行。执行之后，LoginFlow 会调用内部的操作函数，或者直接调用页面对象方法，完成用户登录的操作。

伪代码的第 9-12 行，用和 2-6 行类似的方式调用了 SearchBookFlow，完成了书籍搜索的操作。

9: SearchBookFlowParameters searchBookFlowParameters = new SearchBookFlowParameters();

10: searchBookFlowParameters.setBookName("bookname");

11: SearchBookFlow searchBookFlow = new SearchBookFlow(searchBookFlowParameters);

12: searchBookFlow.withStartPage(loginFlow.getEndPage()).execute();

需要特别注意的是，第 12 行中 withStartPage(loginFlow.getEndPage()) 的含义是，SearchBookFlow 的起始页面将会使用之前 loginFlow 的结束页面。显然，通过这种方式可以很方便地完成两个业务流程之间的页面衔接。

同时，从中还可以看出，其实每个业务流程都可以接受不同的起始页面。以 SearchBookFlow 为例，它的起始页面既可以是书籍首页，也可以是其他页面，但是需要在它的内部对不同的初始页面做出相应的处理，以保证这个业务流程真正开始的页面是在书籍搜索页面。

同样，由于业务流程存在分支的可能性，每个业务流程执行完成的最终页面也不是唯一的，你可以使用 getEndPage 方法拿到这个业务流程执行结束后的最后页面。

通过这段代码的解读，你可以很清楚地理解，业务流程之间的页面衔接是如何实现的。

伪代码的第 15-18 行，调用了 CheckoutBookFlow，完成了书籍购买操作。

15: CheckoutBookFlowParameters checkoutBookFlowParameters = new CheckoutBookFlowParameters();

16: checkoutBookFlowParameters.setBookID(searchBookFlow.getOutPut().getBookID());

17: CheckoutBookFlow checkoutBookFlow = new CheckoutBookFlow(checkoutBookFlowParameters);

18: checkoutBookFlow.withStartPage(searchBookFlow.getEndPage()).execute();

第 15 行，初始化了 CheckoutBookFlow 对应的 checkoutBookFlowParameters 的实例。

第 16 行，通过 setBookID(searchBookFlow.getOutPut().getBookID())，将上一个业务流程 searchBookFlow 的输出参数，作为了当前业务流程的输入参数。这是典型的业务流程之间如何传递参数的示例，也是很多测试场景中都要用到的。

第 17 行，用 checkoutBookFlowParameters 参数实例来初始化 checkoutBookFlow。

第 18 行，通过 execute 方法发起执行。这里需要注意的是，checkoutBookFlow 的起始页面将会使用之前 searchBookFlow 的结束页面。开始执行后，checkoutBookFlow 会调用内部的操作函数，或者直接调用页面对象方法，完成书籍的购买操作。

伪代码的第 21-22 行，调用 LogoutFlow，完成了用户登出操作。

21: LogoutFlow logoutFlow = new LogoutFlow();

22: logoutFlow.withStartPage(checkoutBookFlow.getEndPage()).execute();

第 21 行，由于 LogoutFlow 不带参数，所以直接初始化了 LogoutFlow。

第 22 行，通过 execute 方法发起执行。这里 LogoutFlow 的起始页面将会使用之前 CheckoutBookFlow 的结束页面。开始执行后，LogoutFlow 会调用内部的操作函数，或者直接调用页面对象方法，完成用户登出操作。

通过对这些代码的解读，我解释了业务流程是什么，并从使用者的角度分析了它的主要特点。比如，如何实现不同业务流程间的页面衔接，如何在不同的业务流程间传递参数等。

为了加深印象，我再来总结一下业务流程的优点：

业务流程（Business Flow）的封装更接近实际业务；

基于业务流程的测试用例非常标准化，遵循“参数准备”、“实例化 Flow”和“执行 Flow”这三个大步骤，非常适用于测试代码的自动生成；

由于更接近实际业务，所以可以很方便地和 BDD 结合。BDD 就是 Behavior Driven Development，即行为驱动开发，我会在后续文章中详细讲解。

总结

我以如何把控操作函数的粒度，和如何衔接两个操作函数之间的页面，这两个问题为引子，为你介绍了业务流程的概念、核心思想和适用的场景。

业务流程抽象是，基于操作函数的更接近于实际业务的更高层次的抽象方式。基于业务流程抽象实现的测试用例往往具有较好的灵活性，可以根据实际测试需求方便地组装出各种测试用例。

业务流程的核心思想是，从业务的维度来指导测试业务流程的封装。由于业务流程封装通常很贴近实际业务，所以特别适用于组装面向终端用户的端到端（E2E）的系统功能测试用例，尤其适用于业务功能非常多，并且存在各种组合的 E2E 测试场景。

思考题

你所在公司的 GUI 自动化测试是否已经运用了业务流程级别的封装？在使用过程中，你是否遇到什么瓶颈，是如何解决的？

欢迎你给我留言。

15 | 过不了的坎：聊聊GUI自动化过程中的测试数据

在前面几篇文章中，我从页面操作的角度介绍了 GUI 自动化测试，讲解了页面对象模型和业务流程封装，今天我将从测试数据的角度再来谈谈 GUI 自动化测试。

为了顺利进行 GUI 测试，往往需要准备测试数据来配合测试的进行，如果不采用事先数据准备的方式，测试效率将会大打折扣，而且还会引入大量不必要的依赖关系。

以“用户登录”功能的测试为例，如果你的目的仅仅是测试用户是否可以正常登录，比较理想的方式是这个用户已经存在于被测系统中了，或者你可以通过很方便的方式在测试用例中生成这个用户。否则，难道你要为了测试用户登录功能，而以 GUI 的方式当场注册一个新用户吗？显然，这是不可取的。

其实从这里，你就可以看出测试数据准备是实现测试用例解耦的重要手段，你完全不必为了测试 GUI 用户登录功能而去执行用户注册，只要你能够有方法快速创建出这个登录用户就可以了。

在正式讨论测试数据的创建方法前，我先来分析一下 GUI 测试中两种常见的数据类型：

第一大类是，测试输入数据，也就是 GUI 测试过程中，通过界面输入的数据。比如“用户登录”测试中输入的用户名和密码就就属于这一类数据；再比如，数据驱动测试中的测试数据，也是指这一类。

第二大类是，为了完成 GUI 测试而需要准备的测试数据。比如，“用户登录”测试中，我们需要事先准备好用户账户，以便进行用户的登录测试。今天我分享的测试数据创建的方法，也都是围着这一部分的数据展开的。

那么接下来，我就带你一起去看看创建测试数据的方法都有哪些，以及它们各自的优缺点，和适用场景。

从创建的技术手段上来讲，创建测试数据的方法主要分为三种：

API 调用；

数据库操作；

综合运用 API 调用和数据库操作。

从创建的时机来讲，创建测试数据的方法主要分为两种：

测试用例执行过程中，实时创建测试数据，我们通常称这种方式为 On-the-fly。

测试用例执行前，事先创建好“开箱即用”的测试数据，我们通常称这种方式为 Out-of-box。

在实际项目中，对于创建数据的技术手段而言，最佳的选择是利用 API 来创建数据，只有当 API 不能满足数据创建的需求时，才会使用数据库操作的手段。

实际上，往往很多测试数据的创建是基于 API 和数据库操作两者的结合来完成，即先通过 API 创建基本的数据，然后调用数据库操作来修改数据，以达到对测试数据的特定要求。

而对于创建数据的时机，在实际项目中，往往是 On-the-fly 和 Out-of-box 结合在一起使用。

对于相对稳定的测试数据，比如商品类型、图书类型等，往往采用 Out-of-box 的方式以提高效率；而对于那些只能一次性使用的测试数据，比如商品、订单、优惠券等，往往采用 On-the-fly 的方式以保证不存在脏数据问题。

接下来，我就先从测试数据创建的技术手段开始今天的分享吧。

基于 API 调用创建测试数据

先看一个电商网站“新用户注册”的例子，当用户通过 GUI 界面完成新用户注册信息填写后，向系统后台递交表单，系统后台就会调用 createUser 的 API 完成用户的创建。

而互联网产品，尤其是现在大量采用微服务架构的网站，这个 API 往往以 Web Service 的形式暴露接口。那么，在这种架构下，你完全可以直接调用这个 API 来创建新用户，而无须再向后台递交表单。

由于 API 通常都有安全相关的 token 机制来保护，所以实际项目中，通常会把对这些 API 的调用以代码的形式封装为测试数据工具（Test Data Utility）。

这种方式最大的好处就是，测试数据的准确性直接由产品 API 保证，缺点是并不是所有的测试数据都有相关的 API 来支持。

另外，对需要大量创建数据的测试来说，基于 API 调用方式的执行效率，即使采用了并发机制也不会十分理想。为了解决执行效率的问题，就有了基于数据库操作的测试数据创建手段。

基于数据库操作创建测试数据

实际项目中，并不是所有的数据都可以通过 API 的方式实现创建和修改，很多数据的创建和修改直接在产品代码内完成，而且并没有对外暴露供测试使用的接口。

那么，这种情况下，你就需要通过直接操作数据库的方式来产生测试数据。

同样地，我们可以把创建和修改数据的相关 SQL 语句封装成测试数据工具，以方便测试用例的使用。但是，如果你正尝试在实际项目中运用这个方法，不可避免地会遇到如何才能找到正确的 SQL 语句来创建和修改数据的问题。

因为，创建或修改一条测试数据往往会涉及很多业务表，任何的遗漏都会造成测试数据的不准确，从而导致有些测试因为数据问题而无法进行。

那么，现在我就提供两个思路来帮你解决这个问题：

手工方式。查阅设计文档和产品代码，找到相关的 SQL 语句集合。或者，直接找开发人员索要相关的 SQL 语句集合。

自动方式。在测试环境中，先在只有一个活跃用户的情况下，通过 GUI 界面操作完成数据的创建、修改，然后利用数据库监控工具获取这段时间内所有的业务表修改记录，以此为依据开发 SQL 语句集。

需要注意的是，这两种思路的前提都是，假定产品功能正确，否则就会出现“一错到底”的尴尬局面。

基于数据库操作创建测试数据的最大好处是，可以创建和修改 API 不支持的测试数据，并且由于是直接数据库操作，执行效率会远远高于 API 调用方法。

但是，数据库操作这种方式的缺点也显而易见，数据库表操作的任何变更，都必须同步更新测试数据工具中的 SQL 语句。

但很不幸的是，在实际项目中，经常出现因为 SQL 语句更新不及时而导致测试数据错误的问题，而且这里的数据不准确往往只是局部错误，因此这类问题往往比较隐蔽，只有在特定的测试场景下才会暴露。

所以，在实际工程项目中，需要引入测试数据工具的版本管理，并通过开发流程来保证 SQL 的变更能够及时通知到测试数据工具团队。

综合运用 API 调用和数据库操作创建测试数据

你如果已经理解了基于 API 调用和基于数据库操作创建测试数据这两类方法，那么综合运用这两类方法，就是使得测试数据工具能够提供更多种类的业务测试数据。

具体来讲，当你要创建一种特定的测试数据时，你发现没有直接 API 支持，但是可以通过 API 先创建一个基本的数据，然后再通过修改数据库的方式来更新这个数据，以此来达到创建特定测试数据的要求。

比如，你需要创建一个已经绑定了信用卡的新用户，如果创建新用户有直接的 API，而绑定信用卡需要操作数据库，那这种情况下就需要综合运用这两种方式完成测试数据工具的开发。

实时创建数据：On-the-fly

GUI 测试脚本中，在开始执行界面操作前，我们往往会通过调用测试数据工具实时创建测试数据，也就是 On-the-fly 方式。

这种方式不依赖被测试系统中的任何原有数据，也不会对原有数据产生影响，可以很好地从数据层面隔离测试用例，让测试用例实现“自包含”。

从理论上讲，On-the-fly 是很好的方法，但在实际测试项目中却并不是那么回事儿，往往会存在三个问题：

在用例执行过程中实时创建数据，导致测试的执行时间比较长。 我曾经粗略统计过一个大型 Web GUI 自动化测试项目的执行时间，将近 30% 的时间都花在了测试数据的准备上。

业务数据的连带关系，导致测试数据的创建效率非常低。 比如，你需要创建一个订单数据，而这个订单必然会绑定买家和卖家，以及订单商品信息。
如果完全基于 On-the-fly 模式，你就需要先实时创建买家和卖家这两个用户，然后再创建订单中的商品，最后才是创建这个订单本身。
显然，这样的测试数据创建方式虽然是“自包含”的，但创建效率非常低，会使得测试用例执行时间变得更长，而这恰恰与互联网产品的测试策略产生冲突。

更糟糕的情况是，实时创建测试数据的方式对测试环境的依赖性很强。 比如，你要测试用户登录功能，基于 On-the-fly 方式，你就应该先调用测试数据工具实时创建一个用户，然后再用这个用户完成登录测试。
这时，创建用户的 API 由于各种原因处于不可用的状态（这种情况在采用微服务架构的系统中很常见），那么这时就会因为无法创建用户，而无法完成用户登录测试。

基于这三种常见问题，实际项目中还会引入 Out-of-box 方式（即在执行测试用例前，预先创建好测试数据）准备测试数据。

事先创建测试数据：Out-of-box

Out-of-box 的含义是开箱即用，也就是说，已经在被测系统中预先创建好了充足的、典型的测试数据。这些数据通常是在搭建测试环境时通过数据库脚本“预埋”在系统中的，后续的测试用例可以直接使用。

Out-of-box 的方式有效解决了 On-the-fly 的很多问题，但是这种方法的缺点也很明显，主要体现在以下三个方面：

测试用例中需要硬编码（hardcode）测试数据，额外引入了测试数据和用例之间的依赖。

只能被一次性使用的测试数据不适合 Out-of-box 的方式。 测试用例往往会需要修改测试数据，而且有些测试数据只能被一次性使用。比如，一个商品被买下一次后就不能再用了；再比如，优惠券在一个订单中被使用后，就失效了，等等。所以如果没有很好的全局测试数据管理，很容易因为测试数据失效而造成测试失败。

“预埋”的测试数据的可靠性远不如实时创建的数据。 在测试用例执行过程中，经常会出现测试数据被修改的情况。比如，手动测试，或者是自动化测试用例的调试等情况。

On-the-fly 和 Out-of-box 的互补

基于 On-the-fly 和 Out-of-box 的优缺点和互补性，在实际的大型测试项目中，我们往往会采用两者相结合的方式，从测试数据本身的特点入手，选取不同的测试数据创建方式。

针对应该选择什么时机创建测试数据，结合多年的实践经验，我为你总结了以下三点：

对于相对稳定、很少有修改的数据，建议采用 Out-of-box 的方式，比如商品类目、厂商品牌、部分标准的卖家和买家账号等。

对于一次性使用、经常需要修改、状态经常变化的数据，建议使用 On-the-fly 的方式。

用 On-the-fly 方式创建测试数据时，上游数据的创建可以采用 Out-of-box 方式，以提高测试数据创建的效率。以订单数据为例，订单的创建可以采用 On-the-fly 方式，而与订单相关联的卖家、买家和商品信息可以使用 Out-of-box 方式创建。

其实，为了更好地解决测试数据本身组合的复杂性和多样性，充分发挥测试数据工具的威力，还有很多大型企业的最佳实践值得讨论，在本专栏后面的测试数据章节，我会再为你详细介绍。

总结

今天我从创建测试数据的技术手段和时机两个方面，介绍了 GUI 测试数据的准备。

在实际测试项目中，往往需要综合运用 API 调用和数据库操作来创建测试数据，并且会根据测试数据自身的特点，分而治之地采用 On-the-fly 和 Out-of-box 的方式，以寻求数据稳定性和数据准备效率之间的最佳平衡。

思考题

你所在的公司是如何准备 GUI 测试的测试数据的？遇到了哪些问题，对应的有哪些解决方案呢？

欢迎你给我留言。

16 | 脑洞大开：GUI测试还能这么玩（Page Code Gen + Data Gen + Headless）？

在前面的几篇文章中，我介绍了 GUI 自动化测试的数据驱动测试、页面对象（Page Object）模型、业务流程封装，以及测试数据相关的内容。

今天这篇文章，我将从页面对象自动生成、GUI 测试数据自动生成、无头浏览器三个方面展开，这也是 GUI 测试中三个比较有意思的知识点。

页面对象自动生成

在前面的文章中，我已经介绍过页面对象（Page Object）模型的概念。页面对象模型，是以 Web 页面为单位来封装页面上的控件以及控件的部分操作，而测试用例基于页面对象完成具体操作。最典型的模式就是：XXXPage.YYYComponent.ZZZOperation。

基于页面对象模型的伪代码示例，如图 1 所示。

图 1 基于页面对象模型的伪代码示例

如果你在实际项目中已经用过页面对象模型，你会发现开发和维护页面对象的类（Page Class），是一件很耗费时间和体力的事儿。

你需要打开页面，识别出可以唯一确定某元素的属性或者属性集合，然后把它们写到 Page Class 里，比如图 1 的第 2 行代码 username_input=findElementByName(“username”)，就是通过控件的名字（username）来定位元素的。

更糟糕的是，GUI 的页面会经常变动，如果开发人员开发前端代码时没有严格遵循可测试性的要求，Page Class 的维护成本就会更高。

那么，什么方法能够解决这个问题呢？答案就是，页面对象自动生成技术，它非常适用于需要维护大量页面对象的中大型 GUI 自动化测试项目。

页面对象自动生成技术，属于典型的“自动化你的自动化”的应用场景。它的基本思路是，你不用再手工维护 Page Class 了，只需要提供 Web 的 URL，它就会自动帮你生成这个页面上所有控件的定位信息，并自动生成 Page Class。

但是，需要注意的是，那些依赖于数据的动态页面对象也会被包含在自动生成的 Page Class 里，而这种动态页面对象通常不应该包含在 Page Class 里，所以，往往需要以手工的方式删除。

目前，很多商用自动化工具，比如 UFT，已经支持页面对象自动生成功能了，同时还能够对 Page Class 进行版本管理。

但是，开源的自动化方案，页面对象自动生成功能一般需要自己开发，并且需要与你所用的自动化测试框架深度绑定。目前，中小企业很少有自己去实现这一功能的。

不过，有个好消息是，目前国内应用还不算多、免费的 Katalon Studio，已经提供了类似的页面对象库管理功能，如果感兴趣的话，你可以去试用一下。

GUI 测试数据自动生成

GUI 测试数据自动生成，指的由机器自动生成测试用例的输入数据。

乍一听上去是不是感觉有点玄乎？机器不可能理解你的业务逻辑，怎么可能自动生成测试数据呢？

你的这个想法完全合理，并且也是完全正确的。所以，我在这里说的“测试数据自动生成”，仅仅局限于以下两种情况：

根据 GUI 输入数据类型，以及对应的自定义规则库自动生成测试输入数据。 比如，GUI 界面上有一个“书名”输入框，它的数据类型是 string。
那么，基于数据类型就可以自动生成诸如 Null、SQL 注入、超长字符串、非英语字符等测试数据。
同时，根据自定义规则库，还可以根据具体规则生成各种测试数据。这个自定义规则库里面的规则，往往反映了具体的业务逻辑。比如，对于“书名”，就会有书名不能大于多少个字符、一些典型的书名（比如，英文书名、中文书名等）等等业务方面的要求，那么就可以根据这些业务要求来生成测试数据。
根据自定义规则生成测试数据的核心思想，与安全扫描软件 AppScan 基于攻击规则库自动生成和执行安全测试的方式，有异曲同工之处。

对于需要组合多个测试输入数据的场景，测试数据自动生成可以自动完成多个测试数据的笛卡尔积组合，然后再以人工的方式剔除掉非法的数据组合。
但是，这种方式并不一定是最高效的。对于输入参数比较多，且数据之间合法组合比较少或者难以明确的情况，先自动化生成笛卡尔积组合，再删除非法组合，效率往往还不如人为组合来得高。所以，在这个场景下是否要用测试数据自动生成方法，还需要具体问题具体分析。
更常见的用法是，先手动选择部分输入数据进行笛卡尔积，并删除不合法的部分；然后，在此基础上，再人为添加更多业务上有意义的输入数据组合。
比如，输入数据有 A、B、C、D、E、F 六个参数，你可以先选取最典型的几个参数生成笛卡尔积，假设这里选取 A、B 和 C；然后，在生成的笛卡尔积中删除业务上不合法的组合；最后，再结合 D、E 和 F 的一些典型取值，构成更多的测试输入数据组合。

无头浏览器

无头浏览器，即 Headless Browser，是一种没有界面的浏览器。

什么？浏览器没有界面，还叫什么浏览器啊？别急，我将为你一一道来。

无头浏览器，其实是一个特殊的浏览器，你可以把它简单地想象成是运行在内存中的浏览器。它拥有完整的浏览器内核，包括 JavaScript 解析引擎、渲染引擎等。

与普通浏览器最大的不同是，无头浏览器执行过程中看不到运行的界面，但是你依然可以用 GUI 测试框架的截图功能截取它执行中的页面。

无头浏览器的主要应用场景，包括 GUI 自动化测试、页面监控以及网络爬虫这三种。在 GUI 测试过程中，使用无头浏览器的好处主要体现在四个方面：

测试执行速度更快。 相对于普通浏览器来说，无头浏览器无需加载 CSS 以及渲染页面，在测试用例的执行速度上有很大的优势。

减少对测试执行的干扰。 可以减少操作系统以及其他软件（比如杀毒软件等）不可预期的弹出框，对浏览器测试的干扰。

简化测试执行环境的搭建。 对于大量测试用例的执行而言，可以减少对大规模 Selenium Grid 集群的依赖，GUI 测试可以直接运行在无界面的服务器上。

在单机环境实现测试的并发执行。 可以在单机上很方便地运行多个无头浏览器，实现测试用例的并发执行。

但是，无头浏览器并不完美，它最大的缺点是，不能完全模拟真实的用户行为，而且由于没有实际完成页面的渲染，所以不太适用于需要对于页面布局进行验证的场景。同时，业界也一直缺乏理想的无头浏览器方案。

在 Google 发布 Headless Chrome 之前，PhantomJS 是业界主流的无头浏览器解决方案。但是，这个项目的维护一直以来做得都不够好，已知未解决的缺陷数量多达 1800 多个，虽然支持主流的 Webkit 浏览器内核，但是依赖的 Chrome 版本太低。所以，无头浏览器一直难以在 GUI 自动化测试中大规模应用。

但好消息是，2017 年 Google 发布了 Headless Chrome，以及与之配套的 Puppeteer 框架，Puppeteer 不仅支持最新版本的 Chrome，而且得到 Google 官方的支持，这使得无头浏览器可以在实际项目中得到更好的应用。

也正是这个原因，PhantomJS 的创建者 Ariya Hidayat 停止了它的后续维护，Headless Chrome 成了无头浏览器的首选方案。

那什么是 Puppeteer 呢？Puppeteer 是一个 Node 库，提供了高级别的 API 封装，这些 API 会通过 Chrome DevTools Protocol 与 Headless Chrome 的交互达到自动化操作的目的。

Puppeteer 也是由 Google 开发的，所以它可以很好地支持 Headless Chrome 以及后续 Chrome 的版本更新。

如果你也迫不及待地想要尝试把 Headless Chrome 应用到自己的 GUI 测试中，那还等什么，赶紧下载并开始吧。

总结

我分别介绍了无头浏览器、页面对象自动生成，以及 GUI 测试数据自动生成，这三个 GUI 测试中比较有意思的知识点，包括它们的概念、应用场景等内容。

对于页面对象自动生成，商用测试软件已经实现了这个功能。但是，如果你选择开源测试框架，就需要自己实现这个功能了。

GUI 测试数据自动生成，主要是基于测试输入数据的类型以及对应的自定义规则库实现的，并且对于多个测试输入数据，可以基于笛卡尔积来自动组合出完整的测试用例集合。

对于无头浏览器，你可以把它简单地想象成运行在内存中的浏览器，它拥有完整的浏览器内核。与普通浏览器最大的不同是，它在执行过程中看不到运行的界面。目前，Headless Chrome 结合 Puppeteer 是最先进的无头浏览器方案，如果感兴趣，你可以下载试用。

思考题

在你的工作中，还有哪些好的方法和实践可以提高 GUI 自动化测试的效率吗？

欢迎你给我留言。

17 | 精益求精：聊聊提高GUI测试稳定性的关键技术

不知不觉，我已经介绍完了 GUI 测试相关的知识点，你可以先回顾一下这些知识点，是否还有不清楚的地方，也欢迎你给我留言进行讨论。同时，我希望这些知识点，已经帮你搭建了 GUI 自动化测试的知识体系。

那么，今天我将从实际工程应用的角度，和你一起聊聊 GUI 测试的稳定性问题。

如果你所在的公司已经规模化地开展了 GUI 测试，那我相信你们也一定遇到过测试稳定性的问题。GUI 自动化测试稳定性，最典型的表现形式就是，同样的测试用例在同样的环境上，时而测试通过，时而测试失败。 这也是影响 GUI 测试健康发展的一个重要障碍，严重降低了 GUI 测试的可信性。

所以，今天我分享的主题就是，如何提高 GUI 测试的稳定性。虽然从理论上来讲，GUI 测试有可能做到 100% 稳定，但在实际项目中，这是一个几乎无法达到的目标。根据我的经验，如果能够做到 95% 以上的稳定性，就已经非常不错了。

要提高 GUI 测试稳定性，首先你需要知道到底是什么原因引起的不稳定。你必须找出尽可能多的不稳定因素，然后找到每一类不稳定因素对应的解决方案。

为此，根据我的实践经验，以及所遇到的场景，我为你总结了五种造成 GUI 测试不稳定的因素：

非预计的弹出对话框；

页面控件属性的细微变化；

被测系统的 A/B 测试；

随机的页面延迟造成控件识别失败；

测试数据问题。

并且，我提供了针对这五种不稳定因素的解决思路。

非预计的弹出对话框

非预计的弹出对话框，一般包含两种场景 ;

GUI 自动化测试用例执行过程中，操作系统弹出的非预计对话框， 有可能会干扰 GUI 测试的自动化执行。
比如，GUI 测试运行到一半，操作系统突然弹出杀毒软件更新请求、病毒告警信息、系统更新请求等对话框。这种对话框的弹出往往是难以预计的，但是一旦发生就有可能造成 GUI 自动化测试的不稳定。

被测软件本身也有可能在非预期的时间弹出预期的对话框， GUI 自动化测试有可能会因此而失败。
比如，被测软件是一个电子商务网站，你在网站上进行操作时，很可能会随机弹出“用户调查”对话框。虽然这种对话框是可知的，但是具体会在哪一步弹出却是不可预期的。而这，往往会造成 GUI 自动化测试的不稳定。

怎么解决这类问题呢？

先试想一下，如果你在手工测试时，遇到了这种情况，会如何处理？很简单啊，直接点击对话框上的“确认”或者“取消”按钮，关闭对话框，然后继续相关的业务测试操作。

对 GUI 自动化测试来说，也是同样的道理。具体做法是：

当自动化脚本发现控件无法正常定位，或者无法操作时，GUI 自动化框架自动进入“异常场景恢复模式”。

在“异常场景恢复模式”下，GUI 自动化框架依次检查各种可能出现的对话框，一旦确认了对话框的类型，立即执行预定义的操作（比如，单击“确定”按钮，关闭这个对话框），接着重试刚才失败的步骤。

需要注意的是：这种方式只能处理已知可能出现的对话框。而对于新类型的对话框，只能通过自动化的方式尝试点击上面的按钮进行处理。每当发现一种潜在会弹出的对话框，我们就把它的详细信息（包括对象定位信息等）更新到“异常场景恢复”库中，下次再遇到相同类型的对话框时，系统就可以自动关闭了。

页面控件属性的细微变化

如果页面控件的属性发生了变化，哪怕只是细微的变化，也会导致测试脚本的定位元素失效。

比如，“登录”按钮的 ID 从“Button_Login_001”变成了“Button_Login_888”，那么如果 GUI 自动化测试脚本还是按照原来的“Button_Login_001”来定位“登录”按钮，就会因为 ID 值的变化，定位不到它了，自动化测试用例自然就会失败。

如何解决这个问题呢？还是先试想一下，如果手动操作时遇到了这个问题会怎么处理，然后再把手动处理的方式用编程语言实现。

当“登录”按钮的 ID 从“Button_Login_001”变成了 “Button_Login_888”，你手动操作时可能一眼就发现了。那你是怎么做到一眼发现的呢？

细想一下，你会发现人的思维过程应该是这样的：

你发现页面上的按钮（Button）就那么几个，而且从 ID 中包含的关键字（Login）可以看出是“登录”按钮，再加上这个按钮的 ID 是“Button_Login_001”，“Button_Login_888”怎么看都是同一个对象，只是 ID 最后的数字发生了变化而已。

现在，我来提炼一下这个定位控件的思路：

通过控件类型（Button）缩小了范围；

通过属性值中的关键字（Login）进一步缩小范围；

根据属性值变化前后的相似性，最终定位到该控件。

看到这里，你得到什么启发了吗？

采用“组合属性”定位控件会更精准，而且成功率会更高，如果能在此基础上加入“模糊匹配”技术，可以进一步提高控件的识别率。

“模糊匹配”是指，通过特定的相似度算法，控件属性发生细微变化时，这个控件依旧可以被准确定位。

目前，一些商用 GUI 自动化测试工具，比如 UFT，已经实现了模糊匹配。通常情况下，你只需要启用“模糊匹配”选项即可。如果某个对象的定位是通过模糊匹配完成的，那么，测试报告中将会显示该信息，明确告知此次对象识别是基于模糊匹配完成的，因为 GUI 自动化工具并不能保证每次模糊匹配都一定正确。

但是，开源的 GUI 自动化测试框架，目前还没有现成的框架直接支持模糊匹配，通常需要你进行二次开发，实现思路是：实现自己的对象识别控制层，也就是在原本的对象识别基础上额外封装一层，在这个额外封装的层中加上模糊匹配的实现逻辑。

通常，我不建议把模糊匹配逻辑以硬编码的方式写在代码里，而是引入规则引擎，将具体的规则通过配置文件的方式与代码逻辑解耦。

被测系统的 A/B 测试

A/B 测试，是互联网产品常用的一种测试方法。它为 Web 或 App 的界面或流程提供两个不同的版本，然后让用户随机访问其中一个版本，并收集两个版本的用户体验数据和业务数据，最后分析评估出最好的版本用于正式发布。

A/B 测试通常会发布到实际生产环境，所以就会造成生产环境中 GUI 自动化测试的不稳定。

这种问题的解决思路是，在测试脚本内部对不同的被测版本做分支处理，脚本需要能够区分 A 和 B 两个的不同版本，并做出相应的处理。

随机的页面延迟造成控件识别失败

随机的页面延迟，也是 GUI 测试防不胜防的。既然是随机的，也就是说我们没有办法去控制它，那有没有什么办法去减少它造成的影响呢？

一个屡试不爽的办法就是，加入重试（retry）机制。重试机制是指，当某一步 GUI 操作失败时，框架会自动发起重试，重试可以是步骤级别的，也可以是页面级别的，甚至是业务流程级别的。

对于开源 GUI 测试框架，重试机制往往不是自带的功能，需要自己二次开发来实现。

比如，eBay 的 GUI 自动化测试框架，分别实现了步骤级别、页面级别和业务流程级别的重试机制，默认情况下启用的是步骤级别的重试，页面级别和业务流程级别的重试可以通过测试发起时的命令行参数进行指定。

需要特别注意的是，对于那些会修改一次性使用数据的场景，切忌不要盲目启用页面级别和业务流程级别的重试。

测试数据问题

测试数据问题，也是造成 GUI 自动化测试不稳定的一个重要原因。

比如，测试用例所依赖的数据被其他用例修改了；再比如，测试过程中发生错误后自动进行了重试操作，但是数据状态已经在第一次执行中被修改了。

这样的场景还有很多，我会在后面的测试数据准备系列文章中详细展开，并分析由此引入的测试不稳定性问题的解决思路。

总结

根据我的实践经验，我归纳了五种造成 GUI 自动化测试不稳定的主要因素，并给出了对应的解决思路。

对于非预计的弹出对话框引起的不稳定，可以引入“异常场景恢复模式”来解决。

对于页面控件属性的细微变化造成的不稳定，可以使用“组合属性”定位控件，并且可以通过“模糊匹配技术”提高定位识别率。

对于 A/B 测试带来的不稳定，需要在测试用例脚本中做分支处理，并且需要脚本做到正确识别出不同的分支。

对于随机的页面延迟造成的不稳定，可以引入重试机制，重试可以是步骤级别的，也可以是页面级别的，甚至是业务流程级别的。

对于测试数据引起的不稳定，我在这里没有详细展开，留到后续的测试数据准备系列文章中做专门介绍。

思考题

在工作中，你还遇到过哪些造成 GUI 测试不稳定的因素，你又是如何来解决的？

欢迎你给我留言。

18 | 眼前一亮：带你玩转GUI自动化的测试报告

在 GUI 自动化测试系列的文章中，我围绕着 GUI 自动化测试进行了各种讨论：从最原始的 GUI 测试谈起，逐渐引入了脚本与数据的解耦，并谈论了页面对象模型，以及在此基础上的业务流程模型，接着分享了一些 GUI 自动化测试过程中的新技术，最后和你讨论了 GUI 自动化测试的稳定性问题。

今天，我会再和你聊聊 GUI 自动化测试过程中另外一个很实用的部分：GUI 自动化测试报告。

GUI 测试报告是 GUI 自动化测试的重要组成部分，当有任何的测试用例执行失败时，我们首先就会去分析测试报告，希望从中看到测试用例到底是在哪一步出错了，错误发生时被测系统是在哪个页面上，并且前序步骤又是哪些页面等等。

早期的基于视频的 GUI 测试报告

为了分析测试用例的执行过程与结果，早期就出现了基于视频的 GUI 测试报告。也就是说，GUI 自动化测试框架会对测试执行整个过程进行屏幕录像并生成视频。

这种基于视频的测试报告可以提供清晰的 GUI 测试执行上下文，看起来也很不错。但是，这种方式主要的问题是：

报告的体积通常都比较大，小的几 MB，大的上百 MB，这对测试报告的管理和实时传输非常不利。

分析测试报告时，往往需要结合测试用例以及服务端的日志信息，视频报告这一点上也有所欠缺。

所以，理想中的 GUI 测试报告应该是由一系列按时间顺序排列的屏幕截图组成，并且这些截图上可以高亮显示所操作的元素，同时按照执行顺序配有相关操作步骤的详细描述。

但是，早期的商业 GUI 自动化测试软件也只是具备最基本的顺序截图，并不具备高亮所操作元素的功能，后来商用工具厂商根据用户的实际使用反馈，逐渐完善和改进。

目前，商业的 GUI 自动化测试软件，比如使用最为广泛的 UFT（就是以前的 QTP），已经自带了截图以及高亮显示操作元素功能。也就是说，使用 UFT 执行一个 GUI 自动化测试用例，你无需做任何额外的工作，就能得到一份比较理想的 GUI 测试报告。

开源 GUI 测试框架的测试报告实现思路

但是，如果你使用的是开源软件，比如 Selenium WebDriver，那就需要自己去实现截图以及高亮显示操作元素的功能。实现的思路通常是：

利用 Selenium WebDriver 的 screenshot 函数在一些特定的时机（比如，页面发生跳转时，在页面上操作某个控件时，或者是测试失败时，等等）完成界面截图功能。

具体到代码实现，通常有两种方式：

扩展 Selenium 原本的操作函数；

在相关的 Hook 操作中调用 screenshot 函数。

下面，我会分别针对这两个实现方式，给出具体的示例，帮你理解并实现这个功能。

第一，扩展 Selenium 原本的操作函数实现截图以及高亮显示操作元素的功能

既然 Selenium 原生的 click 操作函数并不具备截图以及高亮显示操作元素的功能，那我们就来实现一个自己 click 函数。

当自己实现的 click 函数被调用时：

首先，用 Javascript 代码高亮显示被操作的元素，高亮的实现方式就是利用 JavaScript 在对象的边框上渲染一个 5-8 个像素的边缘；

然后，调用 screenshot 函数完成点击前的截图；

最后，调用 Selenium 原生的 click 函数完成真正的点击操作。

那么，以后凡是需要调用 click 函数时，都直接调用这个自己封装的 click 函数，直接得到高亮了被操作对象的界面截图。

如图 1 所示，就是用这种方式产生的界面截图，图中依次显示了登录过程中每一个操作的控件，第一张高亮了“Username”的输入框，因为自动化代码会在“Username”框中输入用户名；第二张高亮了“Password”的输入框，因为自动化代码会在“Password”框中输入密码；第三张高亮了”Sign in“按钮，因为自动化代码会去点击这个按钮。

图 1 GUI 界面的时间顺序截图示例

第二，在相关的 Hook 操作中调用 screenshot 函数实现截图以及高亮显示操作元素的功能

其实使用 Hook 的方法比较简单和直观，但是你首先要理解什么是 Hook。

Hook 中文的意思是“钩子”，直接通过定义介绍什么是“钩子”会有些难以理解，那么我就通过一个实例来跟你解释一下。

当执行某个函数 F 时，系统会在执行函数 F 前先隐式执行一个空实现的函数，那么当你需要做一些扩展或者拦截时，就可以在这个空实现的函数中加入自定义的操作了。那么这个空实现的函数就是所谓的 Hook 函数。

这样的例子有很多，比如 Java 的 main 函数，系统在执行 main 函数之前会先在后台隐式执行 premain 函数；JUnit 和 TestNG，都有所谓的 BeforeTest 和 AfterTest 方法，这些都是可以在特定步骤的前后插入自定义操作的接口。

说到这里，你可能已经知道要怎么做了：我可以在这些 Hook 函数中添加截图、元素高亮，以及额外的任意操作，比如更多的详细日志输出等等。

另外，我在前面的文章中分享了基于业务流程的脚本封装，你可以再思考一下，如何在 GUI 报告中体现出业务流程的概念，这样的测试报告会具有更好的可读性。

比如，图 2 所示的 GUI 测试报告就显示了具体的 Flow 名称。这个功能，就是通过 Hook 函数实现的。

具体的实现逻辑也比较简单的，就是在 Flow 开始的第一个 Hook 函数中调用增加报告页的函数，并在这个新增的报告页中输出 Flow 的名字。

图 2 在 GUI 测试报告中体现业务流程

上面所讲的 GUI 测试报告都是针对一个国家的，当面对多个国家站点的 GUI 测试时，事情就会变得更加复杂，你就必须去考虑全球化 GUI 测试报告应该如何设计。

全球化 GUI 测试报告的创新设计

所谓全球化测试是指，同一个业务在全球各个国家都有自己网站。比如，一些大型全球化电商企业在很多国家都有自己的站点，那么对这些站点的测试除了要关注基本的功能，以及各个国家特有的功能外，还要去验证界面布局以及翻译在上下文环境中是否合适。

早期的做法是，雇佣当地的测试工程师，由他们手工执行主要的业务场景测试，并验证相关的页面布局，以及翻译内容与上下文中的匹配度。在当地专门雇佣的这些测试工程师，被称为 LQA。

显然，聘请 LQA 的效率非常低，主要原因是：全部测试工作都由 LQA 在项目后期手工执行，执行前还需要对他们进行业务培训；同时，我们需要准备非常详尽的测试用例文档，LQA 也要花很大的精力去截图并完成最终的测试报告。

为了解决这种低效的模式，最好的解决方法就是：利用 GUI 自动化测试工具生成完整的测试执行过程的截图。这样，LQA 就不再需要去手工执行测试用例了，而是直接分析测试报告中业务操作过程中 GUI 界面截图就可以了，然后发现页面布局问题或者是不恰当的翻译问题。

这个方案看起来已经比较完美了，LQA 的工作重点也更清晰了，但这并不是最优的方案。因为这些 LQA 在实际工作中，还会有以下三个比较痛苦的地方：

需要经常在多个国家的测试报告之间来回切换去比较页面布局；

需要频繁切换到美国网站（也就是主站）的报告，去比较翻译内容与上下文的匹配度；

发现缺陷后，还是需要从 GUI 测试报告中复制截图，并用图像软件标注有问题的点，然后才能打开缺陷管理系统递交缺陷报告。

为了解决这三个问题，我建议你建立以下形式的测试报告。这里有一张图片展示了一份包含多国语言比较报告的示例，听音频的用户可以点击文稿查看如图 3 所示。

图 3 多国语言比较报告

报告的横向，是一个国家的业务测试顺序截图，比如图中第一行是英国网站的登录业务流程顺序截图，第二行是德国网站的登录业务流程顺序截图。报告的纵向，展示的自然就是同一界面在不同国家的形式了。

整个报告可以用键盘上下左右依次移动。可想而知，这样的 GUI 测试报告设计一定可以大幅提高 LQA 的效率。

同时，由于这个 GUI 测试报告是基于 Web 展现的，所以我们可以在测试报告中直接提供递交缺陷的按钮，一旦发现问题直接递交缺陷，同时还可以把相关截图一起直接递交到缺陷管理系统，这将更大程度地提高整体效率。

那么，怎么才能在技术上实现测试报告和缺陷管理系统的交互呢？其实，现今的缺陷管理系统往往都有对外暴露 API 接口，我们完全可以利用这些 API 接口来实现自己的缺陷递交逻辑。

这种测试报告的形式就是 eBay 在全球化站点测试中采用的方案，目前已经取得了很好地效果，降低了工作量的同时，还大幅度提高了全球化测试的质量。

总结

好了，希望上面的测试报告设计方法可以让你有眼前一亮的感觉。接下来，我总结一下今天的主要知识点。

早期基于视频的 GUI 测试报告由于体积较大，而且不能比较方便地和日志适配，所以并不是最好的解决方案。理想的 GUI 测试报告应该是由一系列按时间顺序的屏幕截图组成，并且可以在这些截图上高亮你所操作的元素，同时按照执行时序配有相关操作步骤的详细描述。

商业 GUI 自动化测试框架的 GUI 测试报告已经做得非常成熟，通常不需要做额外的定制或者开发。但是开源 GUI 自动化测试框架的 GUI 测试报告往往需要自己来开发，主要使用了扩展 Selenium 原本的操作函数的方式以及 Hook 函数来实现。

最后，我介绍了 eBay 面对全球化测试过程中 GUI 测试报告的创新设计，希望你也可以借鉴这种方法。

思考题

如果自己去开发 GUI 测试报告的功能，你还能想到其他更多实用的功能吗？你又是如何实现这些功能的？

欢迎你给我留言。

19 | 真实的战场：如何在大型项目中设计GUI自动化测试策略

在前面的文章中，我介绍过 GUI 自动化测试的页面对象模型和业务流程封装等相关知识，也提到过大型全球化电商网站的 GUI 自动化测试，那如何把已经学到的 GUI 测试理论知识用到大型全球化电商网站的测试中呢？

今天，我的分享就从“实战”这个角度展开，带你看看实际的大型全球化电商网站的 GUI 自动化测试如何开展。这场实战，我将从以下两个方面展开：

测试策略如何设计？这一点，我会根据亲身经历的实际项目，和你探讨 GUI 测试的分层测试策略。

测试用例脚本如何组织？需要注意的是，对于这个问题，我不是要和你讨论测试用例的管理，而是要讨论测试用脚本的管理。比如，当需要组装上层的端到端（E2E）测试时，如何才能最大程度地重用已有的页面对象以及业务流程（business flow）。

如果你所在的企业或者项目正在大规模开展 GUI 测试，并且准备使用页面对象模型以及业务流程封装等最佳实践的话，那么，你很可能会遇到本文所描述的问题并且迫切需要相应的解决办法。

大型全球化电商网站的前端模块划分

在正式讨论大型全球化电商网站的 GUI 自动化测试策略设计之前，我先简单介绍一下电商网站的前端架构，为避免过多的技术细节引起不必要的干扰，我只会概要性地介绍与 GUI 自动化测试密切相关的部分。

由于大型全球化电商网站的业务极其庞大，所以前端架构也要按照不同的业务模块来划分，比如用户管理模块、商户订单管理模块、商户商品管理模块等等。

当然由于这些前端模块都会使用项目自己封装的组件库，比如自定义开发的列表组件、登录组件、信用卡组件等，我们通常会把自定义开发的这些所有组件都放在一个“公共组件库”中，为前端模块提供依赖。

所以，从代码库（Repository）的角度来看，各个前端模块都有各自独立的代码库，除此之外还会有一个公共组件的代码库。

大型全球化电商网站的 GUI 自动化测试策略设计

了解了大型全球化电商网站前端模块的划分后，我们再来看看它的 GUI 自动化测试策略是如何设计的。

总体来看，对大型网站来讲，GUI 自动化测试往往应该做得比较轻量级，而不应该把大量的功能测试，以及功能的组合测试放在 GUI 自动化测试中，正如我在第 11 篇文章《互联网产品的测试策略应该如何设计？》中谈到的，GUI 测试通常只覆盖最核心且直接影响主营业务流程的 E2E 场景。

但同时，GUI 的验证一定不是在系统全部完成后才真正开展的，也应该是分阶段、分层次来设计制定测试策略的，那么接下来我也将会按照自底向上的顺序分层次介绍 GUI 自动化的测试策略。

首先，要从前端组件的级别来保证质量，也就是需要对那些自定义开发的组件进行完整全面的测试。

公共组件库会被很多上层的前端模块依赖，它的质量将直接影响这些上层模块的质量，所以我们往往会对这些公共组件进行严格的单元测试。最常用的方案是：基于 Jest 开展单元测试，并考量 JavaScript 的代码覆盖率指标。

Jest 是由 Facebook 发布的，是一个基于 Jasmine 的开源 JavaScript 单元测试框架，是目前主流的 JavaScript 单元测试方案。

完成单元测试后，往往还会基于被测控件构建专用的测试页面，在页面层面再次验证控件相关的功能和状态。这部分测试工作也需要采用自动化的形式实现，具体的做法是：

先构建一个空页面，并加入被测控件，由此可以构建出一个包含被测控件的测试页面，这个页面往往被称为 Dummy Page；

从黑盒的角度出发，在这个测试页面上通过手工和自动化的方式操作被测控件，并验证其功能的正确性。

对于自动化的部分，需要基于 GUI 自动化测试框架开发对应的测试用例。这些测试用例，往往采用和 GUI E2E 一样的测试框架，也是从黑盒的角度来对被测控件做功能验证。

其次，每一个前端模块，都会构建自己的页面对象库，并且在此基础上封装开发自己的业务流程脚本。这些业务流程的脚本，可以组装成每个前端模块的测试用例。

以用户管理模块为例，测试用例的组装过程如下：

首先，把用户管理模块中涉及到的所有页面，比如登录页面、用户注册页面等，按照页面对象模型的要求写成 Page 类；

然后，利用这些 Page 类封装业务流程脚本，比如用户登录流程，用户注册流程等；

最后，在 GUI 测试用例脚本中，调用封装好的业务流程脚本构成该模块的 GUI 测试用例。

在这个阶段，测试用例需要完整覆盖该模块的所有业务逻辑以及相关的功能测试点，但是并不会实现所有测试用例的自动化。

自动化测试用例的原则，通常是：优先选取业务关键路径以及 Happy Path 作为自动化测试的范围。在资源充裕的情况下，我们希望这个阶段的自动化率可以达到 70%-80%。 所以，前端模块的质量保证主要依赖这部分测试。

如果你比较细心，一定还记得我在之前的文章中有提到过，“GUI 的自动化测试往往只覆盖最核心且直接影响主营业务流程的 E2E 场景“，并且”GUI 测试遵循“手工测试为主，自动化为辅”的策略，而这里又建议说理想的自动化率应该达到 70%~80%，是不是有点前后矛盾的感觉。

其实，这是两个层面的测试，这里 70%-80% 的 GUI 自动化覆盖率是针对模块级别的要求；而“自动化测试为辅，手工为主，以及只覆盖核心业务场景”针对的是系统级别的 E2E 测试。这里容易引起混淆的点是模块测试和系统级别 E2E 测试都是属于 GUI 自动化测试的范畴。

最后，组合各个前端模块，并站在终端用户的视角，以黑盒的方式使用网站的端到端（E2E）测试。 这部分的测试主要分为两大部分：

一部分是，通过探索式测试的方法手工执行测试，目标是尽可能多地发现新问题；

另一部分是，通过 GUI 自动化测试执行基本业务功能的回归测试，保证网站核心业务相关的所有功能的正确性。

虽然这部分端到端 GUI 测试用例的绝对数量不多，往往是几百个的规模，但是对于保证最终网站的质量却起着非常关键的作用。

可以这样说，如果这些端到端的 GUI 自动化测试用例 100% 通过，那么上线后基本业务功能的质量就不会有大问题。所以，这部分测试工作的重要性不言而喻。

那么，接下来的问题是，应该由谁来开发这部分端到端的 GUI 自动化测试用例呢？

每个前端模块都会有对应的 Scrum 团队，他们会负责开发该模块的页面对象模型、业务流程脚本以及测试用例。而端到端的 GUI 自动化测试不隶属于任何一个 Scrum 团队。

这种情况下，最好的做法就是：成立一个专门的测试团队，负责这种系统级别的 GUI 测试。这样的团队，往往被称为 E2E 测试团队。

很显然，如果由 E2E 团队从无到有地开发这部分 GUI 自动化测试的脚本，效率低下。而且，这部分测试会涉及很多前端模块，当各个前端模块的需求、业务流程以及页面实现有任何变动时，E2E 团队都很难做到及时更新。

所以，解决这个问题的最佳实践就是：E2E 团队应该尽可能地利用各个模块已有的页面对象和业务流程脚本，组装端到端的 GUI 测试。

这样一方面最大程度地减少了重复工作，另一方面可以把各个模块的变更及时反映到端到端的 GUI 测试中，因为端到端的 GUI 测试用例是直接调用各个模块的页面对象和业务流程脚本，而这些页面对象和业务流程脚本都是由每个模块自己的 Scrum 团队维护的。

而为了能够在端到端的 GUI 自动化测试中，复用各个模块的页面对象和业务流程脚本，我们就必须考虑的问题，也就是我今天要和你探讨的第二个话题：GUI 自动化测试脚本应该如何组织？

大型全球化电商网站的 GUI 自动化测试脚本管理

原有的方案，不能解决端到端的 GUI 自动化测试复用各个模块的页面对象和业务流程脚本的问题，在不断的实践中，我总结了一个如图 1 所示的脚本组织结构来解决这个问题。

图 1 大型全球化电商网站的 GUI 自动化测试脚本管理

也就是说，将各个模块的页面对象和业务流程脚本放在各自的代码库中，并引入页面对象和业务流程脚本的版本管理机制，通常采用页面对象和业务流程脚本的版本号和开发版本号保持一致的方案。

比如模块 A 的版本号是 V1.0.0，那么对应的页面对象库和业务流程脚本的版本号也应该是 V1.0.0。

在端到端的 GUI 自动化测试脚本中，引用各个模块正确的页面对象和业务流程脚本的版本号，测试用例代码就可以直接调用模块的页面对象和业务流程脚本了。

具体在测试项目中，模块版本的依赖往往是用 POM 来配置的，如图 2 展示了一个典型测试项目的 POM 文件中的版本依赖关系，其中引用了两个模块，appcommon 模块对应的就是上文提到的“公共组件库”，而 app.buy 对应的就是具体依赖的前端模块。

由于这只是一个示例，所以我只保留了两个依赖模块，实际的端到端 GUI 测试项目往往会包含大量的模块依赖。

图 2 典型测试项目的 POM 文件中的版本依赖关系

在这种管理机制下，E2E 团队不需要重复开发任何的页面对象和业务流程脚本，而且可以始终保证与各个模块的最新实现同步，同时端到端的 GUI 测试用例脚本也会比较稳定，不会因为各个模块的改动而频繁地修改。

这样一来，E2E 团队就会有更多的时间和精力去设计并执行探索式测试，发现更多的潜在缺陷，形成良性循环。

总结

我从实战的角度，介绍了大型全球化电商网站 GUI 测试的策略设计以及测试脚本管理的问题：

首先，要从前端组件的级别来保证质量，也就是需要对那些自定义开发的组件进行完整全面的测试。通常前端组件会基于 Jest 做比较严格的单元测试。

其次，每一个前端模块，都会构建自己的页面对象库，并且在此基础上封装开发自己的业务流程脚本。这些业务流程的脚本，可以组装成每个前端模块的测试用例。

最后，把各个前端模块组合在一起之后，站在终端用户的视角以黑盒的方式使用网站的端到端的测试。端到端的测试应该尽可能多地重用各个模块的页面对象库和业务流程脚本来完成。

而为了能够在端到端的 GUI 自动化测试中，复用各个模块的页面对象和业务流程脚本，我建议的方案是：对各个前端业务模块的页面对象库和业务流程脚本，实施版本化管理机制。

思考题

你所在的公司或者项目团队，是否已经或者正计划开展 E2E GUI 测试？开展过程中，遇到过什么难题，你们又是如何解决的？

欢迎你给我留言。

20 | 与时俱进：浅谈移动应用测试方法与思路

你好，我是茹炳晟。我今天分享的主题是“与时俱进：浅谈移动应用测试方法与思路”。

在 GUI 自动化测试这个系列，我讲了很多基于浏览器的业务测试的内容，你可能会说，现在移动 App 大行其道，对移动应用测试的方法和思路才更重要。

确实，现今移动互联网蓬勃发展，很多互联网应用的流量大部分已经不是来自于传统 PC 端的 Web 浏览器，而是来自于移动端。

图 1 展示了最近 12 个月来亚洲地区的流量分布统计，可见，现如今将近三分之二的流量是来自于手机端的，剩下的三分之一来自于传统 PC 端，还有很少一部分流量来自于平板电脑（其实这部分也可以归为移动端）。

图 1 Mobile 端和 PC 端流量统计数据

但是，在我看来无论是移动端测试还是 PC 端测试，都属于 GUI 测试的范畴，所以基本的测试思路，比如基于页面对象封装和基于业务流程封装的思想是相通的，之前介绍的那些脚本分层的实现方法也都同样适用于移动端的 GUI 测试。

与此同时，移动端应用的测试也会因为其自身特点，有一些独特的测试方法与思路。严格来讲，移动端应用又可以进一步细分为三大类：Web App、Native App 和 Hybrid App。所以，我今天分享的内容重点就是，这三类移动应用的测试方法，以及移动专项测试的思路与方法。

三类移动应用的特点

Web App 指的是移动端的 Web 浏览器， 其实和 PC 端的 Web 浏览器没有任何区别，只不过 Web 浏览器所依附的操作系统不再是 Windows 和 Linux 了，而是 iOS 和 Android 了。

Web App 采用的技术主要是，传统的 HTML、JavaScript、CSS 等 Web 技术栈，当然现在 HTML5 也得到了广泛的应用。另外，Web App 所访问的页面内容都是放在服务器端的，本质上就是 Web 网页，所以天生就是跨平台的。

Native App 指的是移动端的原生应用， 对于 Android 是 apk，对于 iOS 就是 ipa。Native App 是一种基于手机操作系统（iOS 和 Android），并使用原生程序编写运行的第三方应用程序。

Native App 的开发，Android 使用的语言通常是 Java，iOS 使用的语言是 Objective-C。通常来说，Native App 可以提供比较好的用户体验以及性能，而且可以方便地操作手机本地资源。

Hybrid App（俗称：混血应用），是介于 Web App 和 Native App 两者之间的一种 App 形式。

Hybrid App 利用了 Web App 和 Native App 的优点，通过一个原生实现的 Native Container 展示 HTML5 的页面。更通俗的讲法可以归结为，在原生移动应用中嵌入了 Webview，然后通过该 Webview 来访问网页。

Hybrid App 具有维护更新简单，用户体验优异以及较好的跨平台特性，是目前主流的移动应用开发模式。

图 2 三类移动应用的架构原理

三类不同移动应用的测试方法

了解了 Web App、Native App 和 Hybrid App 这三类应用的特性，接下来，我就跟你说说它们的测试方法。

好了，我们已经知道了移动应用的三个主要种类，接下来我们从测试的角度再来看看这三类不同的移动应用。

对于 Web App，显然其本质就是 Web 浏览器的测试，我在前面文章中介绍的所有 GUI 自动化测试的方法和技术，比如数据驱动、页面对象模型、业务流程封装等，都适用于 Web App 的测试。

如果你的 Web 页面是基于自适应网页设计（即符合 Responsive Web 设计的规范），而且你的测试框架如果支持 Responsive Page，那么原则上你之前开发的运行在 PC Web 端的 GUI 自动化测试用例，不做任何修改就可以直接在移动端的浏览器上直接执行，当然运行的前提是你的移动端浏览器必须支持 Web Driver。

其中，自适应网页设计（Responsive Web Design）是指，同一个网页能够自动识别屏幕分辨率、并做出相应调整的网页设计技术。比如，图 3 所示的例子就是同一个网页在不同分辨率下的不同展示效果。

图 3 自适应网页设计实例

对 Native App 的测试，虽然不同的平台会使用不同的自动化测试方案（比如，iOS 一般采用 XCUITest Driver，而 Android 一般采用 UiAutomator2 或者 Espresso 等），但是数据驱动、页面对象以及业务流程封装的思想依旧适用，你完全可以把这些方法应用到测试用例设计中。

对 Hybrid App 的测试，情况会稍微复杂一点，对 Native Container 的测试，可能需要用到 XCUITest 或者 UiAutomator2 这样的原生测试框架，而对 Container 中 HTML5 的测试，基本和传统的网页测试没什么区别，所以原本基于 GUI 的测试思想和方法都能继续适用。

唯一需要注意的是，Native Container 和 Webview 分别属于两个不同的上下文（Context），Native Container 默认的 Context 为“NATIVE APP"，而 Webview 默认的 Context 为“WEBVIEW_+ 被测进程名称”。

所以，当需要操作 Webview 中的网页元素时，需要先切换到 Webview 的 Context 下，如图 4 所示代码就完成了这一切换操作。

图 4 Hybrid App 中切换 Context 的代码示例

如此看来，移动端的测试除了使用的测试框架不同以外，测试设计本身和 GUI 测试有异曲同工之妙，似乎并没有什么新的内容，那真的是这样吗？

答案显然是否定的。

移动应用专项测试的思路和方法

对于移动应用，顺利完成全部业务功能测试往往是不够的。如果你的关注点只是业务功能测试，那么，当你的移动应用被大量用户安装和使用时，就会暴露出很多之前完全没有预料到的问题，比如：

流量使用过多；

耗电量过大；

在某些设备终端上出现崩溃或者闪退的现象；

多个移动应用相互切换后，行为异常；

在某些设备终端上无法顺利安装或卸载；

弱网络环境下，无法正常使用；

Android 环境下，经常出现 ANR(Application Not Responding)；

…

这样的问题还有很多，为了避免或减少此类情况的发生，所以移动应用除了进行常规的功能测试外，通常还会进行很多移动应用所特有的专项测试。

今天这篇文章，我就从交叉事件测试、兼容性测试、流量测试、耗电量测试、弱网络测试、边界测试这 6 个最主要的专项测试来展开。

第一，交叉事件测试

交叉事件测试也叫中断测试，是指 App 执行过程中，有其他事件或者应用中断当前应用执行的测试。

比如，App 在前台运行过程中，突然有电话打进来，或者收到短信，再或者是系统闹钟等等情况。所以，在 App 测试时，就需要把这些常见的中断情况考虑在内，并进行相关的测试。

注意，此类测试目前基本还都是采用手工测试的方式，并且都是在真机上进行，不会使用模拟器。

首先，采用手工测试的原因是，此类测试往往场景多，而且很多事件很难通过自动化的方式来模拟，比如呼入电话、接收短信等，这些因素都会造成自动化测试的成本过高，得不偿失，所以工程实践中，交叉事件测试往往全是基于手工的测试。

其次，之所以采用真机，是因为很多问题只会在真机上才能重现，采用模拟器测试没有意义。

交叉事件测试，需要覆盖的场景主要包括：

多个 App 同时在后台运行，并交替切换至前台是否影响正常功能；

要求相同系统资源的多个 App 前后台交替切换是否影响正常功能，比如两个 App 都需要播放音乐，那么两者在交替切换的过程中，播放音乐功能是否正常；

App 运行时接听电话；

App 运行时接收信息；

App 运行时提示系统升级；

App 运行时发生系统闹钟事件；

App 运行时进入低电量模式；

App 运行时第三方安全软件弹出告警；

App 运行时发生网络切换，比如，由 Wifi 切换到移动 4G 网络，或者从 4G 网络切换到 3G 网络等；

…

其实你可以发现，这些需要覆盖的场景，也是我们今后测试的测试用例集，每一场景都是一个测试用例的集合。

第二，兼容性测试

兼容性测试顾名思义就是，要确保 App 在各种终端设备、各种操作系统版本、各种屏幕分辨率、各种网络环境下，功能的正确性。常见的 App 兼容性测试往往需要覆盖以下的测试场景：

不同操作系统的兼容性，包括主流的 Andoird 和 iOS 版本；

主流的设备分辨率下的兼容性；

主流移动终端机型的兼容性；

同一操作系统中，不同语言设置时的兼容性；

不同网络连接下的兼容性，比如 Wifi、GPRS、EDGE、CDMA200 等；

在单一设备上，与主流热门 App 的兼容性，比如微信、抖音、淘宝等；

…

兼容性测试，通常都需要在各种真机上执行相同或者类似的测试用例，所以往往采用自动化测试的手段。 同时，由于需要覆盖大量的真实设备，除了大公司会基于 Appium + Selenium Grid + OpenSTF 去搭建自己的移动设备私有云平台外，其他公司一般都会使用第三方的移动设备云测平台完成兼容性测试。

第三方的移动设备云测平台，国外最知名的是 SauceLab，国内主流的是 Testin。

第三，流量测试

由于 App 经常需要在移动互联网环境下运行，而移动互联网通常按照实际使用流量计费，所以如果你的 App 耗费的流量过多，那么一定不会很受欢迎。

流量测试，通常包含以下几个方面的内容：

App 执行业务操作引起的流量；

App 在后台运行时的消耗流量；

App 安装完成后首次启动耗费的流量；

App 安装包本身的大小；

App 内购买或者升级需要的流量。

流量测试，往往借助于 Android 和 iOS 自带的工具进行流量统计，也可以利用 tcpdump、Wireshark 和 Fiddler 等网络分析工具。

对于 Android 系统，网络流量信息通常存储在 /proc/net/dev 目录下，也可以直接利用 ADB 工具获取实时的流量信息。另外，我还推荐一款 Android 的轻量级性能监控小工具 Emmagee，类似于 Windows 系统性能监视器，能够实时显示 App 运行过程中 CPU、内存和流量等信息。

对于 iOS 系统，可以使用 Xcode 自带的性能分析工具集中的 Network Activity，分析具体的流量使用情况。

但是，流量测试的最终目的，并不是得到 App 的流量数据，而是要想办法减少 App 产生的流量。虽然，减少 App 消耗的流量不是测试工程师的工作，但了解一些常用的方法，也将有助于你的测试日常工作：

启用数据压缩，尤其是图片；

使用优化的数据格式，比如同样信息量的 JSON 文件就要比 XML 文件小；

遇到既需要加密又需要压缩的场景，一定是先压缩再加密；

减少单次 GUI 操作触发的后台调用数量；

每次回传数据尽可能只包括必要的数据；

启用客户端的缓存机制；

…

第四，耗电量测试

耗电量也是一个移动应用能否成功的关键因素之一。

在目前的生态环境下，能提供类似服务或者功能的 App 往往有很多，如果在功能类似的情况下，你的 App 特别耗电、让设备发热比较严重，那么你的用户一定会卸载你的 App 而改用其他 App。最典型的就是地图等导航类的应用，对耗电量特别敏感。

耗电量测试通常从三个方面来考量：

App 运行但没有执行业务操作时的耗电量；

App 运行且密集执行业务操作时的耗电量；

App 后台运行的耗电量。

耗电量检测既有基于硬件的方法，也有基于软件的方法。我所经历过的项目都是采用软件的方法，Android 和 iOS 都有各自自己的方法：

Android 通过 adb 命令“adb shell dumpsys battery”来获取应用的耗电量信息；

iOS 通过 Apple 的官方工具 Sysdiagnose 来收集耗电量信息，然后，可以进一步通过 Instrument 工具链中的 Energy Diagnostics 进行耗电量分析。

第五，弱网络测试

与传统桌面应用不同，移动应用的网络环境比较多样，而且经常出现需要在不同网络之间切换的场景，即使是在同一网络环境下，也会出现网络连接状态时好时坏的情况，比如时高时低的延迟、经常丢包、频繁断线，在乘坐地铁、穿越隧道，和地下车库的场景下经常会发生。

所以，移动应用的测试需要保证在复杂网络环境下的质量。具体的做法就是：在测试阶段，模拟这些网络环境，在 App 发布前尽可能多地发现并修复问题。

在这里，我推荐一款非常棒的开源移动网络测试工具：Facebook 的 Augmented Traffic Control（ATC）。

ATC 最好用的地方在于，它能够在移动终端设备上通过 Web 界面随时切换不同的网络环境，同时多个移动终端设备可以连接到同一个 Wifi，各自模拟不同的网络环境，相互之间不会有任何影响。也就是说，只要搭建一套 ATC 就能满足你所有的网络模拟需求。

如果你对 ATC 感兴趣，可以在它的官方网站找到详细的使用说明。

第六，边界测试

边界测试是指，移动 App 在一些临界状态下的行为功能的验证测试，基本思路是需要找出各种潜在的临界场景，并对每一类临界场景做验证和测试。 主要的场景有：

系统内存占用大于 90% 的场景；

系统存储占用大于 95% 的场景；

飞行模式来回切换的场景；

App 不具有某些系统访问权限的场景，比如 App 由于隐私设置不能访问相册或者通讯录等；

长时间使用 App，系统资源是否有异常，比如内存泄漏、过多的链接数等；

出现 ANR 的场景；

操作系统时间早于或者晚于标准时间的场景；

时区切换的场景；

…

总结

好了，最后我来总结一下今天的主要的知识点：

移动应用根据技术架构的不同，主要分为 Web App、Native App 和 Hybrid App 三大类，这三类应用的测试方法本质上都属于 GUI 测试的范畴。

从业务功能测试的角度看，移动应用的测试用例设计和传统 PC 端的 GUI 自动化测试策略比较类似，只是测试框架不同，数据驱动、页面对象模型和业务流程封装依旧适用；

各种专项测试是移动应用的测试重点，也有别于传统 GUI 测试。专项测试包括：交叉事件测试、兼容性测试、流量测试、耗电量测试、弱网络测试和边界测试。

思考题

请你谈谈对移动应用测试的看法，你所在的企业，是如何开展移动测试的？你们又涉及了哪些类型的专项测试？

欢迎你给我留言。

21 | 移动测试神器：带你玩转Appium

在上一篇文章中，我介绍了 Web App、Native App 和 Hybrid App 三种不同类型的移动应用以及对应的测试设计方法，也介绍了移动应用所特有的专项测试知识。

今天，我就以移动应用的自动化测试为主题，介绍目前主流的移动应用自动化测试框架 Appium。Appium 是一个开源的自动化测试框架，支持 iOS 和 Android 上 Web App、Native App 和 Hybrid App 的自动化测试。

由于基于 Appium 的移动应用环境搭建相对复杂，虽然网上也有不少教程，但是知识点都比较零碎，而且大多都是基于早期版本的示例，所以我会使用最新版本的 Appium Desktop 1.6.2 和 Appium Server 1.8.1 来展开今天的内容：

首先，我会展示如何在 Mac 环境下一步一步地搭建 Appium 测试环境；

接下来，我以 iOS 为例，实际开发两个测试用例，一个是 Native App 的测试用例，另一个是 Web App 的测试用例（因为 Hybird App 的测试用例其实是类似的，Native App 的壳，Web App 的内容，所以就不再单独举例子了）；

然后，我会在 iOS 的模拟器上实际执行这两个测试用例（之所以选择 iOS 模拟器，而不用 iOS 真机做例子，是因为 iOS 真机的测试需要用到 Apple 开发者账号，还需要对被测应用进行签名等，会在环境搭建过程中引入很多额外步骤，而这些步骤对于讲解 Appium 并没有直接的关系）；

最后，当你已经通过实际使用对 Appium 形成感性认识后，我再来简单介绍一下 Appium 的内部原理，让你做到知其然知其所以然。

移动应用的自动化测试需求

在开始设计测试用例前，我们首先需要明确要开发的这两个自动化测试用例的具体测试需求。

Native App 的测试用例，被测 App 我选用了 Appium 官方的示例 App，被测 App 的源代码可以通过“https://github.com/appium/ios-test-app” 下载，然后在 Xcode 中编译打包成 TestApp.app。
具体的测试需求是输入两个数字，然后点击“Compute Sum”验证两个数字相加后的结果是否正确。

Web App 的测试用例，具体需求是在 iPhone 上打开 Safari 浏览器，访问 Appium 的官方主页“http://appium.io”，然后验证主页的标题是否是“Appium: Mobile App Automation Made Awesome”。

图 1 Native App 和 Web App 的 GUI 界面示例

接下来，我将从最初的环境搭建开始，和你来一起开发 iOS 上的 Native App 和 Web App 的测试用例。首先我们看一下 iOS 的环境搭建，如果你之前没有接触过这部分内容，你可以跟着我的步骤一步一步来做；而如果你已经比较熟悉 Xcode 的话，可以跳过这部分内容，直接从“Appium 环境搭建”部分看起。

iOS 环境搭建

在正式搭建 Appium 环境前，我们先来搭建 iOS 开发环境：

首先，下载安装 Xcode；

然后，在 Xcode 中下载 iOS 的模拟器；

接着，使用 Xcode 编译打包被测试 App；

最后，在 iOS 的模拟器中尝试手工执行这两个测试用例。

在 iOS 模拟器中，手动执行测试用例的具体操作步骤如下：

启动 Xcode，导入 ios-test-app 下的 TestApp.xcodeproj 项目。

在 Xcode 中，打开“Preferences”中的“Components”，完成 iOS 10.0 Simulator 的下载。

在 Xcode 的“General”页面，将 TestApp 的“Deployment Target”设置为 10.0，并且将“Devices”设置为“iPhone”，如图 2 所示。

图 2 TestApp 的 General 配置

在 Xcode 中编译运行 TestApp，之后系统会自动启动 iPhone 模拟器，自动完成 TestApp 的安装，并在 iPhone 模拟器中自动启动 TestApp。

在 TestApp 中手动执行自定义的加法测试用例。

退出 TestApp，然后打开 Safari 浏览器，在 Safari 中执行访问 Appium 官方主页的测试用例。

至此，你已经搭建好了 iOS 开发环境，并且成功编译打包了 TestApp。接下来，我们再一起来搭建 Appium 测试环境，并尝试在 Appium 中开发上述的两个测试用例。

Appium 测试环境搭建

通过 Appium 的官方网站下载并安装最新版本的 Appium，截止本文写作的时间，最新版本是 Appium-1.6.2.dmg。

需要注意的是，早期版本和网上很多教程都建议用命令行的形式启动 Appium Server，但在这里我是想强调的是，你完全可以通过界面启动（在 Launchpad 中找到 Appium 的图标，点击即可启动），而且新版本的 Appium 也推荐这个启动方式。通过界面启动，是目前最简单直接的方式。

然后，你需要用命令行“npm install -g appium-doctor”安装 Appium 的环境诊断工具 appium-doctor，用于检查 Appium 所依赖的相关环境变量以及其他安装包是否都已经配置好了。如果还没有，就需要逐个安装，并根据 appium-doctor 的提示配置环境变量。

这里，Appium 最主要的依赖项主要有：Java、Node.js、Xcode、Carthage、Android SDK、adb 等。如果你所有的环境依赖都正常配置的话，你就会看到 appium-doctor 返回这样一个截图，如图 3 所示。

图 3 正常配置环境依赖后，appium-doctor 返回的截图

按照上面的步骤，配置好 Appium 的环境依赖后，就可以继续启动 Appium Server 了。

Appium Inspector 的使用

为了后续测试用例的顺利执行，我们可以先来熟悉一下 Appium Inspector 的使用。Appium Inspector 主要是用来协助对界面元素进行定位的工具。

首先，我们来看看如何使用 Appium Inspector 启动 iPhone 的模拟器，并在模拟器上运行 TestApp，以及如何通过 Inspector 定位 TestApp 界面上的元素（了解元素的定位是后续开发自动化脚本的基础）。具体的操作过程如下。

通过 Appium Server 的“Start Inspector Session”按钮，进入 Session 配置界面。

图 4 点击“Start Inspector Session”按钮打开 Session 配置界面

在 Session 配置界面完成必要参数的配置。这里你需要根据选用的移动设备操作系统、模拟器 / 真机等具体情况来完成参数配置工作。需要配置的参数主要包括：platformName、platformVersion、DeviceName、automationName 和 app。
其中，automationName，指自动化测试框架的名称，这里采用了 XCUITest；app 指被测 Native App 的安装包路径，这里使用之前 Xcode 打包生成的 TestApp.app，这样启动模拟器时，就会自动把 TestApp.app 安装到模拟器中。
其他参数的配置非常简单，我就不再一一展开了。

图 5 Session 配置界面

完成配置后，点击 Session 界面的“Start Session”按钮，启动 iPhone 模拟器，并在 iPhone 模拟器中启动 TestApp，同时还会打开 Inspector 窗口。如图 6 所示。

图 6 启动 Session 后的 Inspector 窗口

在 Inspector 窗口，我们可以利用“Select Elements”功能，通过点击元素显示 Native App 上的元素定位信息。如图 7 所示。

图 7 “Select Elements”功能示例

在 Inspector 窗口，可以通过“Recording”功能生成不同语言的自动化脚本。比如在启用了“Recording”功能后，点击“Compute Sum”按钮，就会生成如图 8 所示的自动化脚本片段。

图 8 “Recording”功能示例

了解了如何通过 Inspector 获取元素定位信息的方法之后，我们就来正式开发基于 Appium 的第一个 Web App 和第一个 Native App 的测试用例。

基于 Appium 开发你的第一个 Native App 的测试用例

第一步，建立一个空的 Maven 项目，然后在 POM 文件中加入如图 9 所示的依赖。

在这个案例里面，我们会使用 TestNG 组织测试用例，所以代码的第 14 行加入了 TestNG 的依赖。

第 19 行的 java-client 是关键，java-client 的作用是利用 Java 代码将测试用例中的操作步骤发送给 Appium Server，然后由 Appium Server 自动完成这些操作。

目前 Appium 支持多种编程语言，每种语言都有自己的 client，比如这里使用 Java 语言，所以引入了 java-client；如果你使用 Python 语言，那么就需要引用 python-client。

图 9 POM 文件加入 TestNG 和 java-client 的依赖

第二步，创建一个类，并命名为“iOS_NativeApp_DemoTest”，然后按照如图 10 所示的代码实现这个 class。

注意，这里的代码是真实的可执行 Java 代码，你可以直接拿去使用。

图 10 Native App 测试用例实例

代码第 21 行的 @BeforeTest，第 38 行的 @AfterTest，以及第 44 行的 @Test，都是利用了 TestNG 的 annotation 对函数进行标注。
标有 @Test 的函数是真正的测试主体，所有测试相关的步骤都放在这个函数中；
标有 @ BeforeTest 的函数会在 @Test 函数之前执行测试的相关准备工作，图中的代码用这个函数完成了 DesiredCapabilities 的设置，并用该 Capabilities 构造了 iosdriver；
标有 @ AfterTest 的函数在 @Test 函数执行结束后执行，主要用于环境的清理和收尾，图示的代码用这个函数完成了 iosdriver 的退出操作。

代码的第 24-33 行构造了 DesiredCapabilities 对象，并对 APPIUM_VERSION、PLATFORM_VERSION、PLATFORM_NAME、AUTOMATION_NAME、DEVICE_NAME 和 APP 等参数进行了设置。其中 APP 的值是被测 Native App 安装包的绝对路径。

代码的第 46-58 行是测试用例的主体部分，主要分为三部分：
第 47-50 行通过 iosdriver 的 findElementByAccessibilityId 方法定义了页面上的四个元素，分别是输入参数框 A、输入参数框 B、计算按钮和加法结果显示框。代码中具体的 AccessibilityId 可以通过 Inspector 获取。
第 53-55 行通过自定义元素的操作执行加法运算。
第 58 行通过断言方法 assertEquals 验证加法运算的结果。

第三步，为了运行这个 TestNG 的测试用例，我们需要再添加一个 testng.xml 文件， 具体内容如图 11 所示。

图 11 testng.xml 文件示例

第四步，在保证 Appium Server 已经启动的情况下，就可以运行 testng.xml 执行测试了。 测试开始后，首先会自动启动基于 iOS 10.0 的 iPhone 7 模拟器，然后依次自动完成 WebDriverAgent（WDA）和被测 Native App 的安装。

WDA 是由 Facebook 开源的支持 iOS 自动化的代理工具，其底层通过 XCUItest 实现自动化。

接着，就会自动运行被测 Native App，并根据 @Test 函数中定义的步骤完成自动化测试的步骤和验证。

到此，我们的第一个基于 Appium 的 Native App 自动化测试用例就设计完了。

基于 Appium 开发你的第一个 Web App 的测试用例

有了 Native App 测试用例的设计基础，再来实现一个基于 Appium 的 Web App 自动化测试用例就简单得多了。

第一步，在上述的 Maven 项目中再创建一个类，并命名为“iOS_WebApp_DemoTest”，然后按照如图 12 所示的代码实现这个类。

图 12 Web App 测试用例实例

代码的整体结构和上述 Native App 测试用例的完全一致，只有一个地方需要特别注意：代码的第 29 行，由于 Web App 是基于浏览器的测试，所以这里不需要指定 App 这个参数，而是直接用 BROWSER_NAME 指定浏览器的名字即可。

对于测试用例的主体部分，也就是代码的第 45-47 行就比较简单了，首先打开 Safari 浏览器并访问“http://appium.io/”，接着用断言方法 assertEquals 验证页面的 Title 是不是“Appium: Mobile App Automation Made Awesome.”。其中，实际页面的 Title，可以通过 mobiledriver 的 getTitle 方法获得。

第二步，在 testng.xml 中添加这个 Web App 的测试用例，然后我们就可以在 Appium Server 已经启动的情况下执行这个测试用例了。

这个测试用例，首先会自动启动基于 iOS 10.0 的 iPhone 7 模拟器，然后自动打开 Safari 浏览器并访问 Appium 的官方网站。执行完成后的界面如下图 13 所示。

图 13 测试用例执行完成的界面

进行到这里，我们基于 Appium 开发的第一个 Web App 的自动化测试用例，也就开发完成了。

经过前面 Appium 环境搭建，以及两个测试用例的设计，相信你已经对 Appium 有了一个感性的认识了。那么，Appium 的实现原理又是怎样的呢？理解了 Appium 的使用原理，可以帮助你更好地使用这个工具，设计更加“有的放矢”的测试用例。

Appium 的实现原理

Appium 作为目前主流的移动应用自动化测试框架，具有极强的灵活性，主要体现在以下 5 个方面：

测试用例的实现支持多种编程语言，比如 Java、Ruby、Python 等；

Appium Server 支持多平台，既有基于 Mac 的版本，也有基于 Windows 的版本；

支持 Web App、Native App 和 Hybird App 三大类移动应用的测试；

既支持 iOS，也支持 Android；

既支持真机，也支持模拟器。

实际应用中，你可以根据项目情况灵活组合完成移动应用的自动化测试。比如，用 Java 写 iOS 上的 Native App 的测试用例，测试用例跑在 Mac 平台的 iPhone 虚拟机上；或者，用 Python 写 Android 上的 Web App 的测试用例，测试用例通过 Windows 平台跑在 Android 的真机上。

这样的组合还有很多很多。那你有没有想过，Appium 为什么可以做到如此强大的灵活性呢？这就要从 Appium 的基本原理讲起了。

要真正理解 Appium 的内部原理，你可以把 Appium 分成三大部分，分别是 Appium Client、Appium Server 和设备端。这三部分的关系如图 14 所示。

图 14 Appium 内部原理

我们先来看看处于中间位置的 Appium Server。

Appium Server 有 Mac 和 Windows 版本，也就是说 Appium Server 可以运行在 Mac 或者 Windows 电脑上。本质上，Appium Server 是一个 Node.js 应用，接受来自 Appium Client 的请求，解析后通过 WebDriver 协议和设备端上的代理打交道。

如果是 iOS，Appium Server 会把操作请求发送给 WebDriverAgent（简称 WDA），然后 WDA 再基于 XCUITest 完成 iOS 模拟器或者真机上的自动化操作；

如果是 Android，Appium Server 会把操作请求发送给 appium-UIautomator2-server，然后 appium-UIautomator2-server 再基于 UIAutomator V2 完成 Android 模拟器或者真机上的自动化操作。

Appium Client 其实就是测试代码，使用对应语言的 Client 将基于 JSON Wire 协议的操作指令发给 Appium Server。

整体来说，Appium 的内部原理可以总结为：Appium 属于 C/S 架构，Appium Client 通过多语言支持的第三方库向 Appium Server 发起请求，基于 Node.js 的 Appium Server 会接受 Appium Client 发来的请求，接着和 iOS 或者 Android 平台上的代理工具打交道，代理工具在运行过程中不断接收请求，并根据 WebDriver 协议解析出要执行的操作，最后调用 iOS 或者 Android 平台上的原生测试框架完成测试。

总结

好了，我来总结一下今天的主要的内容：

目前网络上，Appium 工具使用相关的资料都比较零散，为此我以最新版本的 Appium Desktop 1.6.2 和 Appium Server 1.8.1 为例，手把手地带你搭建了 iOS 环境，以及 Appium 测试环境，并介绍了如何通过 Appium Inspector 来定位页面元素。

搭建好了测试环境后，我分别针对 Native App 和 Web App 这两类移动应用，基于 Appium 实现了两个测试用例，这也是我在这个专栏里面，为你实现的第一个移动应用的测试用例。虽然测试需求比较简单，但是你也可以从中体会到移动应用测试用例设计的思想、方法。

最后，本着知其然知其所以然的原则，我介绍了 Appium 的实现原理：它属于 C/S 架构，Appium Client 通过第三方库向 Appium Server 发起请求，Appium Server 接受请求，然后和移动平台上的代理工具打交道，代理工具在运行过程中不断接收来自 Appium Server 的请求，并解析出要执行的操作，最后调用移动平台原生的测试框架完成测试操作。

思考题

我在这篇文章里面举的例子都是基于 iOS 的，建议你基于 Android 分别实现一个 Web App 和 Native App 的测试用例。

如果实现过程中，遇到了问题，或者有一些自己的想法，请给我留言讨论吧。

22 | 从0到1：API测试怎么做？常用API测试工具简介

你好，我是茹炳晟，我今天分享的主题是“从 0 到 1：API 测试怎么做？常用 API 测试工具简介”。

在第 11 篇文章《互联网产品的测试策略应该如何设计？》中，我介绍过当今互联网产品的测试策略往往会采用菱形结构，即重量级 API 测试，轻量级 GUI 测试，轻量级单元测试，由此可见 API 测试在现今测试中的重要性不言而喻。

这篇文章是 API 自动化测试系列的第一篇文章，我会先为你打好 API 测试的基础。所以，我会先从 0 到 1 设计一个 API 测试用例，通过这个测试用例，你可以体会到最基本的 API 测试是如何进行的，并介绍几款常用的 API 测试工具。

API 测试的基本步骤

通常来讲，无论采用什么 API 测试工具，API 测试的基本步骤主要包括以下三大步骤：

准备测试数据（这是可选步骤，不一定所有 API 测试都需要这一步）；

通过 API 测试工具，发起对被测 API 的 request；

验证返回结果的 response。

对 API 的测试往往是使用 API 测试工具，比如常见的命令行工具 cURL、图形界面工具 Postman 或者 SoapUI、API 性能测试的 JMeter 等。

为了让你更好地理解 API 测试具体是怎么做的，并掌握常见 API 测试工具的使用，我会以基于主流 Spring Boot 框架开发的简单 Restful API 为例，分别介绍如何使用 cURL 和 Postman 对其进行最基本的功能测试，目的是让你对 API 测试有一个基本的感性认识。

基于 Spring Boot 构建的 API

因为基于 Spring Boot 从 0 到 1 构建一个 API，并不是本文的重点，为了不影响你对文章主要内容的把握，我直接采用了一个预先开发好的 Account API 为例展开讲解。你可以从https://github.com/SpectoLabs/spring-cloud-contract-blog下载完整的代码。

这个 Account API 的功能非常简单，就是基于你提供的 ID 值创建一个 Account 对象，并返回这个新创建 Account 对象。

比如，如果你的请求是“account/ID008”，那么返回的 response 就应该是“{“id”:“ID008”,“type”:“friends”,“email”:“robin@api.io”}”。

这个 Account API 的功能逻辑实现非常简单，图 1 和图 2 列出了主要的代码逻辑。

图 1 中，代码的第 21 行说明了 API 的 endpoint 以及对应的操作是 GET 方法，第 22 行明确说明了 GET 方法具体的业务逻辑是由 accountService.getById() 方法实现的。

图 1 RestController 的实现

图 2 中，代码的第 8 行实现了 accountService.getById() 方法，具体逻辑就是返回一个以传入 ID 为 ID 的 Account 对象。

图 2 具体业务逻辑的实现

我推荐使用 IntelliJ 打开这个下载的项目，然后直接启动其中的 account-service。启动成功后，account-service 会运行在本地机器的 8080 端口。启动成功后的界面如图 3 所示。

图 3 成功启动基于 Spring Boot 的 Account API

使用 cURL 命令行工具进行测试

首先，你需要下载安装 cURL，然后就可以通过以下命令发起 Account API 的调用。调用结束后的界面如图 4 所示。

curl -i -H "Accept: application/json" -X GET "http://127.0.0.1:8080/account/ID008"

图 4 使用 cURL 测试 Account API

这行命令中参数的含义如下：

第一个参数“-i”，说明需要显示 response 的 header 信息；

第二个参数“-H”，用于设定 request 中的 header；

第三个参数“-X”，用于指定执行的方法，这里使用了 GET 方法，其他常见的方法还有 POST、PUT 和 DELETE 等，如果不指定“-X”，那么默认的方法就是 GET。

最后“ http://127.0.0.1:8080/account/ID008 ”，指明了被测 API 的 endpoint 以及具体的 ID 值是“ID008”。

当使用 cURL 进行 API 测试时，常用参数还有两个：

“-d”：用于设定 http 参数，http 参数可以直接加在 URL 的 query string，也可以用“-d”带入参数。参数之间可以用“&”串接，或使用多个“-d”。

“-b”：当需要传递 cookie 时，用于指定 cookie 文件的路径。

需要注意的是这些参数都是大小写敏感的。

了解了这几个最常用的参数后，我再来分析一些最常用的 cURL 命令以及使用的场景，包括 Session 的场景和 Cookie 的场景。

第一，Session 的场景

如果后端工程师使用 session 记录使用者登入信息，那么后端通常会传一个 session ID 给前端。之后，前端在发给后端的 requests 的 header 中就需要设置此 session ID，后端便会以此 session ID 识别出前端是属于具体哪个 session，此时 cURL 的命令行如下所示：

curl -i -H "sessionid:XXXXXXXXXX" -X GET "http://XXX/api/demoAPI"

第二，Cookie 的场景

如果是使用 cookie，在认证成功后，后端会返回 cookie 给前端，前端可以把该 cookie 保存成为文件，当需要再次使用该 cookie 时，再用“-b cookie_File” 的方式在 request 中植入 cookie 即可正常使用。具体的 cURL 的命令行如下所示：

// 将 cookie 保存为文件

curl -i -X POST -d username=robin -d password=password123 -c ~/cookie.txt "http://XXX/auth"

// 载入 cookie 到 request 中

curl -i -H "Accept:application/json" -X GET -b ~/cookie.txt "http://XXX/api/demoAPI"

最后，需要特别说明的是，cURL 只能发起 API 调用，而其本身并不具备结果验证能力（结果验证由人完成），所以严格意义上说 cURL 并不属于测试工具的范畴。但是由于 cURL 足够轻量级，经常被很多开发人员和测试人员使用，所以我在这里做了简单的介绍。

接下来，我们再来看看如何使用目前主流的 Postman 完成 API 测试。

使用图形界面工具 Postman 进行测试

Postman 是目前使用最广泛的 Http 请求模拟工具之一，常常被用于 Web Service API 的测试。

早期的 Postman，是以 Chrome 浏览器的插件（plugin）形式存在的，最新版本的 Postman 已经是独立的应用了。我猜想是因为这个工具的应用日益广泛，所以才有了今天的独立版本。

你可以通过官方网站下载对应于 Mac、Windows 和 Linux 操作系统的不同版本，截止文章写作完成时，最新的 Mac 版本是 6.2.2。

接下来，我就会以 Mac 6.2.2 版本为例，跟你分享如何用 Postman 完成你的 API 测试。如果你使用浏览器的 plugin 版本，或者是基于其他操作系统的版本，这都没问题，基本的操作和步骤都是一样的。

具体的操作，主要包括：

发起 API 调用；

添加结果验证；

保存测试用例；

基于 Postman 的测试代码自动生成。

第一步，发起 API 调用

我们的目标是对 Account API 做测试，所以这里你需要选择 Postmant 的“Request”模块。进入相应界面后，你需要按照图 5 的提示依次执行以下三步操作，发起 Account API 的调用。

在 endpoint 输入框中输入“http://127.0.0.1:8080/account/ID_008”；

选择“GET”方法；

点击“Send”按钮发起 API 调用。

图 5 Postman 发起 Account API 的测试

完成以上步骤后，界面如图 6 所示。我们看到返回的 response 默认以 JSON 文件的形式显示在下面的 Body 中。

图 6 Postman 执行 GET 后的界面

这样就完成了一次 Account API 的调用，是不是非常简单。但问题是，这只是一个 API 调用，并没有对调用结果进行自动化验证。接下来，我们就加上结果验证的部分，一起看看会有什么效果。

第二步，添加结果验证

在 Postman 中添加结果验证也非常方便，假定我们在 Account API 测试过程中有以下四个验证点：

请求的返回状态码（Status Code）应该是 200；

请求的响应时间应该小于 200 ms；

请求返回的 response header 中应该包含“Content-Type”参数；

请求返回的 response body 中，“type”的值应该是“friends”；

那么，接下来我们一起来看看如何使用 Postman 来添加这四个验证点。

为此，我们首先打开“Tests”界面，然后在右下角的“SNIPPETS”中依次点击：

“Status code: Code is 200”

“Response time is less than 200 ms”

“Response headers：Content-Type header check”

“Response body: JSON value check”

完成以上操作后，“Tests”中会自动生成验证代码，接着只要按照具体的测试要求，对这些生成的代码进行一些小修改就可以了。

在这个例子中，你只需修改需要验证的 JSON 键值对即可，即代码的第 15 行。修改完成后我们可以再次点击“Send”按钮发起测试。测试通过的界面如图 7 所示，最下面的“Test Results”显示四个测试全部通过。

图 7 测试通过的界面

第三步，保存测试用例

测试通过后，我们往往希望可以把这个测试 request 保存下来，以方便后续使用，为此 Postman 提供了保存测试 request 的功能，并提供了 Collection 来分类管理保存多个测试 request。

Collection 是用来保存测试 request 的一个集合，Collection 内部还可以建立目录结构以方便进一步的分类和管理。

这里我们点击“Save As”按钮，在弹出的对话框中可以建立 Collection，并且可以命名测试 request 并将其保存到 Collection 中。

我建立了“API Test Demo”的 Collection，并且将刚才的测试 request 命名为“AccountAPI”保存到这个 Collection 中。

以后再要使用这个测试 request 时，直接在 Collection 中打开它，即可使用。同时你如果申请注册了一个 Postman 账号，就可以很方便地在多个环境中共享这个 Collection 了。

第四步，基于 Postman 的测试代码自动生成

至此，你已经掌握了 Postman 最基本的使用方法，但还有一个问题没有解决。很多时候，你希望将你的测试 request 作为回归测试用例集成到 CI/CD 的流程中，这就要求可以通过命令行的方式执行你的测试。为了达到这个目的，目前有两种做法：

将 Postman 中的测试 request 用自动化的方式直接转换成 API 测试的代码。 目前 Postman 已经支持这个功能了，可以将保存的测试 request 自动化转换成常见测试框架直接支持的代码，而且支持多语言。
比如，基于 Java 的“OK HTTP”和“Unirest”，基于 Python 的“http.client”和“Requests”，基于 NodeJS 的“Native”“Request”和“Unirest”，基于 JavaScript 的“JQuery AJAX”和“XHR”等等。你可以点击如图 8 所示的“Code”按钮进入代码生成界面。

图 8 自动生成 API 测试代码

利用 Newman 工具直接执行 Postman 的 Collection。 你需要先将 Postman 中的 Collection 导出为 JSON 文件，然后执行以下命令行。

newman run examples/sample-collection.json;

如何应对复杂场景的 API 测试？

我在前面分享的 Restful API 测试案例中，只涉及到了最基本的 API 的测试方法，而且测试场景也很比较简单（只是单个 API 的调用）。

但在实际项目中，除了这种单个 API 的测试场景外，还有很多复杂场景的 API 测试。所以，为了解决你在实际项目中可能会碰到的一些问题，我再和你聊聊目前一些常见的典型复杂场景，以及相应的测试思路和方法。

测试场景一：被测业务操作是由多个 API 调用协作完成

很多情况下，一个单一的前端操作可能会触发后端一系列的 API 调用，由于前端测试的相对不稳定性，或者由于性能测试的要求，你必须直接从后端通过模拟 API 的顺序调用来模拟测试过程。

这时，API 的测试用例就不再是简单的单个 API 调用了，而是一系列 API 的调用，并且经常存在后一个 API 需要使用前一个 API 返回结果的情况，以及需要根据前一个 API 的返回结果决定后面应该调用哪个 API 的情况。

好在，我们已经实现了 API 的调用和结果解析的代码化，这也就意味着我们可以很灵活地直接用代码来处理这些场景了。 比如，通过代码将上个 API 调用返回的 response 中的某个值传递给下一个 API，再比如根据上一个 API 的返回结果决定下一个应该调用哪个 API 等。

除此之外，我们还需要迫切解决的一个问题是：如何才能高效地获取单个前端操作所触发的 API 调用序列。

解决这个问题的核心思路是，通过网络监控的手段，捕获单个前端操作所触发的 API 调用序列。比如，通过类似于 Fiddler 之类的网络抓包工具，获取这个调用序列；又比如，目前很多互联网公司还在考虑基于用户行为日志，通过大数据手段来获取这个序列。

测试场景二：API 测试过程中的第三方依赖

API 之间是存在依赖关系的，比如你的被测对象是 API A，但是 API A 的内部调用了 API B，此时如果由于某种原因，API B 在被测环境中处于不可用状态，那么 API A 的测试就会受到影响。

在单体架构下，通常只会在涉及到第三方 API 集成的场景中才会遇到这个问题，所以还不算严重。但是，在微服务架构下，API 间相互耦合的依赖问题就会非常严重。

解决这个问题的核心思路是，启用 Mock Server 来代替真实的 API。那么，Mock Server 怎么才能真实有效地模拟被替代的 API 呢？这个问题，我会在分享《紧跟时代步伐：微服务模式下 API 测试要怎么做？》这个主题时，和你详细探讨。

测试场景三：异步 API 的测试

异步 API 是指，调用后会立即返回，但是实际任务并没有真正完成，而是需要稍后去查询或者回调（Callback）的 API。

一直以来，异步 API 测试都是 API 测试中比较困难的部分。在我看来，对异步 API 的测试主要分为两个部分：一是，测试异步调用是否成功，二是，测试异步调用的业务逻辑处理是否正确。

异步调用是否成功，这个还比较简单，主要检查返回值和后台工作线程是否被创建两个方面就可以了。

但是，对异步调用业务逻辑的测试就比较复杂了，因为异步 API 通常发生在一些比较慢的操作上，比如数据库 I/O、消息队列 I/O 等，此时测试往往需要去验证数据库中的值、消息队列中的值等，这就需要测试代码具有访问和操作数据库或者消息队列的能力。
在实际工程项目中，这些能力一般会在测试框架级别提供，也就是说要求 API 测试框架中包含对应的工具类去访问和操作数据库或者消息队列等。

总结

通常情况下，无论你采用什么 API 测试工具，基本的测试步骤往往都是三步，即准备测试数据（并不是所有的 API 测试都需要这一步）、通过 API 测试工具发起对被测 API 的 request、验证返回结果的 response。

接下来，我通过一个简单的 Restful API 测试为例，和你分享了 cURL 和 Postman 这两个常用 API 测试工具的使用。

其中，cURL 只具备发起 API 调用的功能，而不具备结果验证能力，所以严格地说它并不属于测试工具的范畴。Postman 常常被用于 Web Service API 的测试具体的操作，测试流程主要包括：发起 API 调用、添加结果验证、保存测试用例、基于 Postman 的测试代码自动生成。

最后，为了帮你应对实际工程项目中复杂的 API 测试场景，我分享了被测业务操作是由多个 API 调用协作完成、API 测试过程中的第三方依赖、异步 API 的测试，这三个复杂场景下的测试思路和方法。

思考题

单个 API 测试是比较简单的，但在实际项目中，往往存在按时序的 API 调用以及异步 API 调用，这类 API 你是如何测试的？遇到过什么难题，又是如何解决的？

感谢你的收听，欢迎给我留言讨论。

23 | 知其然知其所以然：聊聊API自动化测试框架的前世今生

你好，我是茹炳晟，今天我和你分享的主题是“知其然知其所以然：聊聊 API 自动化测试框架的前世今生”。

在上一篇文章中，我以一个简单的 Restful API 为例，分别介绍了 cURL 和 Postman 的使用方法，相信你已经对 API 测试有个感性认识了。

但是，我们不能仅仅停留在感性认识的层面，还需要熟悉并掌握这些测试方法，完成相应的 API 测试工作。所以，也就有了我今天分享的主题，希望可以通过对 API 自动化测试框架发展的介绍，让你理解 API 测试是如何一步一步地发展成今天的样子，以“知其所以然”的方式加深你对 API 自动化测试的理解。

接下来，我将会遵循由简入繁的原则，为你介绍 API 测试框架，以发现问题然后解决问题的思路为主线，展开今天的分享。

早期的基于 Postman 的 API 测试

早期的 API 测试，往往都是通过类似 Postman 的工具完成的。但是，由于这类工具都是基于界面操作的，所以有以下两个问题亟待解决：

当需要频繁执行大量的测试用例时，基于界面的 API 测试就显得有些笨拙；

基于界面操作的测试难以与 CI/CD 流水线集成。

所以，我们迫切需要一套可以基于命令行执行的 API 测试方案。这样，API 测试可以直接通过命令行发起，与 CI/CD 流水线的整合也就方便得多了。

基于 Postman 和 Newman 的 API 测试

于是就出现了集成 Postman 和 Newman 的方案，然后再结合 Jenkins 就可以很方便地实现 API 测试与 CI/CDl 流水线的集成。Newman 其实就是一个命令行工具，可以直接执行 Postman 导出的测试用例。

用 Postman 开发调试测试用例，完成后通过 Newman 执行，这个方案看似很完美。但是在实际工程实践中，测试场景除了简单调用单个 API 以外，还存在连续调用多个 API 的情况。

此时，往往会涉及到多个 API 调用时的数据传递问题，即下一个 API 调用的参数可能是上一个 API 调用返回结果中的某个值。另外，还会经常遇到的情况是，API 调用前需要先执行一些特定的操作，比如准备测试数据等。

因此，对于需要连续调用多个 API 并且有参数传递的情况，Postman+Newman 似乎就不再是理想的测试方案了。

基于代码的 API 测试

为了解决这个问题，于是就出现了基于代码的 API 测试框架。比较典型的是，基于 Java 的 OkHttP 和 Unirest、基于 Python 的 http.client 和 Requests、基于 NodeJS 的 Native 和 Request 等。

小型的互联网企业，往往会根据自己的业务需求，选用这些成熟的 API 测试框架。

但是，对于中大型的互联网企业，一般都会自己开发更适合自身业务上下文的 API 测试框架，比如 eBay，我们为了实现代码化的 API 测试，开发了自己的 HttpClient，后期为了使 API 测试的代码更简洁易懂，就基于 Rest-Assured 封装了全新的 API 测试框架。

这种根据公司业务上下文开发实现的 API 测试框架，在使用上有很多优点，而且灵活性也很好，主要体现在以下几个方面：

可以灵活支持多个 API 的顺序调用，方便数据在多个 API 之间传递，即上一个 API 调用返回结果中的某个字段值可以作为后续 API 调用的输入参数；

方便在 API 调用之前或者之后执行额外的任意操作，可以在调用前执行数据准备操作，可以在调用后执行现场清理工作等；

可以很方便地支持数据驱动测试，这里的数据驱动测试概念和 GUI 测试中的数据驱动测试完全相同，也就是可以将测试数据和测试代码分离解耦；

由于直接采用了代码实现，所以可以更灵活地处理测试验证的断言（Assert）；

原生支持命令行的测试执行方式，可以方便地和 CI/CD 工具做集成。

这里我给出了一段伪代码示例，用于展示如何用代码实现一个简单的 API 测试。

图 1 基于代码的 API 测试的伪代码示例

代码的第 1-12 行，创建了 CreateUserAPI 类，其中包含了 endpoint、操作方法 PUT、InlineParam 和 Param 的设置，并且构建了对应的 request 对象；

代码的第 14-19 行，是测试的主体函数。这段函数的逻辑是这样的：

首先，构建 CreateUserAPI 的对象；

然后，用 CreateUserAPI 对象的 buildRequest 方法结合输入参数构建 request 对象；

接着，通过 request 对象的 request() 方法发起了 API 调用；

最后，验证 response 中的状态码是不是 200。

在这段伪代码中，有以下几点需要你特别注意：

代码中“CreateUserAPI 的父类 RestAPI”“_buildRequest() 方法”“request() 方法”“addInlineParam() 方法”等，都是由 API 测试框架提供的。

为了简化代码，这里并没有引入数据驱动的 data provider。但在实际项目中，代码第 14 行的测试输入参数，往往来自于 data provider，即由数据驱动的方式提供测试输入数据。

由于测试过程完全由代码实现，所以可以很方便的在测试执行前后增加任意的额外步骤。比如，需要在 CreateUser 前增加数据创建的步骤时，只需要在代码第 15 行前直接添加就可以了。

这里的例子只有一个 API 调用，当需要多个 API 顺序调用时，直接扩展 testCreateUser 方法即可，两个 API 之间的数据传递可以通过上一个 API 返回的 response.XXXX 完成。

通过这段伪代码，我们可以看到，虽然基于代码的 API 测试灵活性很好，也可以很方便地和 CI/CD 集成，但是也引入了一些新的问题，比如：

对于单个 API 测试的场景，工作量相比 Postman 要大得多；

对于单个 API 测试的场景，无法直接重用 Postman 里面已经积累的 Collection。

在实际工程中，这两个问题非常重要，而且必须要解决。因为公司管理层肯定无法接受相同工作的工作量直线上升，同时原本已经完成的部分无法继续使用，所以自动化生成 API 测试代码的技术也就应运而生了。

自动生成 API 测试代码

自动生成 API 测试代码是指，基于 Postman 的 Collection 生成基于代码的 API 测试用例。

其实，在上一篇文章《从 0 到 1：API 测试怎么做？常用 API 测试工具简介》最后的部分，我已经提到过 Postman 工具本身已经支持将 Collection 转化成测试代码，但如果直接使用这个功能的话，还有两个问题需要解决：

测试中的断言（assert）部分不会生成代码，也就是说测试代码的生成只支持发起 request 的部分，而不会自动生成测试验证点的代码；

很多中大型互联网企业都是使用自己开发的 API 测试框架，那么测试代码的实现就会和自研 API 测试框架绑定在一起，显然 Postman 并不支持这类代码的自动生成。

鉴于以上两点，理想的做法是自己实现一个代码生成工具，这个工具的输入是 Postman 中 Collection 的 JSON 文件，输出是基于自研 API 框架的测试代码，而且同时会把测试的断言一并转化为代码。

这个小工具实现起来并不复杂，其本质就是解析 Collection JSON 文件的各个部分，然后根据自研 API 框架的代码模板实现变量替换。 具体来讲，实现过程大致可以分为以下三步：

首先，根据自研 API 框架的代码结构建立一个带有变量占位符的模板文件；

然后，通过 JSON 解析程序，按照 Collection JSON 文件的格式定义去提取 header、method 等信息；

最后，用提取得到的具体值替换之前模板文件中的变量占位符，这样就得到了可执行的自研框架的 API 测试用例代码。

有了这个工具后，我建议你的工作模式（Working Model）可以转换成这样：

对于 Postman 中已经累积的 Collection，全部由这个工具统一转换成基于代码的 API 测试用例；

开发人员继续使用 Postman 执行基本的测试，并将所有测试用例保存成 Collection，后续统一由工具转换成基于代码的 API 测试用例；

对于复杂测试场景（比如，顺序调用多个 API 的测试），可以组装由工具转换得到的 API 测试用例代码，完成测试工作。

如图 2 所示，就是一个组装多个由工具转换得到的 API 测试用例代码的例子。其中，代码第 3 行的类“CreateUserAPI”和第 10 行的类“BindCreditCardAPI”的具体代码就可以通过工具转换得到。

图 2 多个 API 顺序调用的测试用例代码

至此，基于代码的 API 测试发展得算是比较成熟了，但在实际应用过程中还有一个痛点一直未被解决，那就是测试验证中的断言，也是我接下来要和你一起讨论的话题。

Response 结果发生变化时的自动识别

在实际的工程项目中，开发了大量的基于代码的 API 测试用例后，你会发现一个让人很纠结的问题：到底应该验证 API 返回结果中的哪些字段？

因为你不可能对返回结果中的每一个字段都写 assert，通常情况下，你只会针对关注的几个字段写 assert，而那些没写 assert 的字段也就无法被关注了。

但对 API 测试来说，有一个很重要的概念是后向兼容性（backward compatibility）。API 的后向兼容性是指，发布的新 API 版本应该能够兼容老版本的 API。

后向兼容性除了要求 API 的调用参数不能发生变化外，还要求不能删减或者修改返回的 response 中的字段。因为这些返回的 response 会被下游的代码使用，如果字段被删减、改名或者字段值发生了非预期的变化，那么下游的代码就可能因为无法找到原本的字段，或者因为字段值的变化而发生问题，从而破坏 API 的后向兼容性。

所以，我们迫切需要找到一个方法，既可以不对所有的 response 字段都去写 assert，又可以监测到 response 的结构以及没有写 assert 的字段值的变化。

在这样的背景下，诞生了“Response 结果变化时的自动识别”技术。也就是说，即使我们没有针对每个 response 字段都去写 assert，我们仍然可以识别出哪些 response 字段发生了变化。

具体实现的思路是，在 API 测试框架里引入一个内建数据库，推荐采用非关系型数据库（比如 MongoDB），然后用这个数据库记录每次调用的 request 和 response 的组合，当下次发送相同 request 时，API 测试框架就会自动和上次的 response 做差异检测，对于有变化的字段给出告警。

你可能会说这种做法也有问题，因为有些字段的值每次 API 调用都是不同的，比如 token 值、session ID、时间戳等，这样每次的调用就都会有告警。

但是这个问题很好解决，现在的解决办法是通过规则配置设立一个“白名单列表”，把那些动态值的字段排除在外。

总结

为了让你可以更好地理解今天的 API 测试框架，我从其发展历程的角度进行了分析：

早期的基于 Postman 的 API 测试在面临频繁执行大量测试用例，以及与 CI/CD 流水线整合的问题时，显得心有余而力不足。为此，基于命令行的 API 测试实践，也就是 Postman+Newman，具有很好的灵活性，解决了这两个问题。

但是，Postman+Newman 的测试方案，只能适用于单个 API 调用的简单测试场景，对于连续调用多个 API 并涉及到参数传递问题时，这个方案就变得不那么理想和完美了。随后，API 测试就过渡到了基于代码的 API 测试阶段。

一些小型企业，则往往会选择适合自己业务的成熟 API 测试框架。中大型的互联网企业，一般都会根据自己的业务上下文，在成熟 API 测试框架的基础上封装自己的 API 测试框架，提升测试效率和灵活性。

但是，不管是采用现成的还是自己去开发 API 测试框架，都会遇到测试用例开发效率低下，以及无法直接重用 Postman 中积累的 Collection 的问题，为此我分享了两个比较好用的方法，也就是：自动生成 API 测试代码和 Response 结果变化的自动识别，并给出了这两个方法的实现思路。

希望我分享的这些内容，可以帮你解决在实际测试项目中遇到的问题。

思考题

目前，基于代码的 API 测试框架已经比较成熟了，所以在此基础上又出现了基于配置文件的 API 测试框架，比如典型的 HttpRunner，在此类 API 测试框架的支持下，测试用例本身往往就是纯粹的配置文件了。你是否有接触过这类 API 测试框架，对此又有什么看法呢？

欢迎你给我留言。

24 | 紧跟时代步伐：微服务模式下API测试要怎么做？

你好，我是茹炳晟，今天我分享的主题是“紧跟时代步伐：微服务模式下 API 测试要怎么做？”。

通过一个的 Restful API 实例，我介绍了 cURL 和 Postman 工具的基本用法，这样我们对 API 测试有了一个感性认识；在此基础上，我介绍了 API 自动化测试框架发展的来龙去脉，借此我们对 API 测试框架的理解又更深入了一层。

今天，我将更进一步，带你去了解当下最热门的技术领域的 API 测试，即微服务模式下的 API 测试。微服务架构下，API 测试的最大挑战来自于庞大的测试用例数量，以及微服务之间的相互耦合。所以，我今天分享这个主题的目的就是，帮你理解这两个问题的本质，以及如何基于消费者契约的方法来应对这两个难题。

而为了掌握微服务模式下的 API 测试，你需要先了解微服务架构（Microservice Architecture）的特点、测试挑战；而要了解微服务架构，你又需要先了解一些单体架构（Monolithic Architecture）的知识。所以，今天的话题我将逐层展开，目的就是希望你可以真正理解，并快速掌握微服务模式下的 API 测试。

单体架构（Monolithic Architecture）

单体架构是早期的架构模式，并且存在了很长时间。单体架构是将所有的业务场景的表示层、业务逻辑层和数据访问层放在同一个工程中，最终经过编译、打包，并部署在服务器上。

比如，经典的 J2EE 工程，它就是将表示层的 JSP、业务逻辑层的 Service、Controller 和数据访问层的 DAO（Data Access Objects），打包成 war 文件，然后部署在 Tomcat、Jetty 或者其他 Servlet 容器中运行。

显然单体架构具有发布简单、方便调试、架构复杂性低等优点，所以长期以来一直被大量使用，并广泛应用于传统企业级软件。

但是，随着互联网产品的普及，应用所承载的流量越来越庞大，单体架构的问题也被逐渐暴露并不断放大，主要的问题有以下几点：

灵活性差：无论是多小的修改，哪怕只修改了一行代码，也要打包发布整个应用。更糟的是，由于所有模块代码都在一起，所以每次编译打包都要花费很长时间。

可扩展性差：在高并发场景下，无法以模块为单位灵活扩展容量，不利于应用的横向扩展。

稳定性差：当单体应用中任何一个模块有问题时，都可能会造成应用整体的不可用，缺乏容错机制。

可维护性差：随着业务复杂性的提升，代码的复杂性也是直线上升，当业务规模比较庞大时，整体项目的可维护性会大打折扣。

正是因为面对互联网应用时，单体架构有这一系列无法逾越的鸿沟，所以催生了微服务架构。

其实，微服务架构也不是一蹴而就的，也经历了很长时间的演化发展，中间还经历了著名的 SOA 架构。但是这个由单体架构到 SOA 架构再到微服务架构的演进过程，并不是本文的重点，所以我就不再详细展开了，如果你感兴趣的话，可以自行去查阅一些相关资料。

微服务架构（Microservice Architecture）

微服务是一种架构风格。在微服务架构下，一个大型复杂软件系统不再由一个单体组成，而是由一系列相互独立的微服务组成。其中，各个微服务运行在自己的进程中，开发和部署都没有依赖。

不同服务之间通过一些轻量级交互机制进行通信，例如 RPC、HTTP 等，服务可独立扩展伸缩，每个服务定义了明确的边界，只需要关注并很好地完成一件任务就可以了，不同的服务可以根据业务需求实现的便利性而采用不同的编程语言来实现，由独立的团队来维护。

图 1 就很形象地展示了单体架构和微服务架构之间的差异。

图 1 单体架构 VS 微服务架构

微服务架构具有以下特点：

每个服务运行在其独立的进程中，开发采用的技术栈也是独立的；

服务间采用轻量级通信机制进行沟通，通常是基于 HTTP 协议的 RESTful API；

每个服务都围绕着具体的业务进行构建，并且能够被独立开发、独立部署、独立发布；

对运维提出了非常高的要求，促进了 CI/CD 的发展与落地。

微服务架构下的测试挑战

由于微服务架构下，一个应用是由很多相互独立的微服务组成，每个微服务都会对外暴露接口，同时这些微服务之间存在级联调用关系，也就是说一个微服务通常还会去调用其他微服务，鉴于以上特点，微服务架构下的测试挑战主要来自于以下两个方面：

过于庞大的测试用例数量；

微服务之间的耦合关系。

接下来，我会针对这两项挑战分别展开，包括它们从何而来，以及如何应对这些挑战，最终完成测试。

第一，过于庞大的测试用例数量

在传统的 API 测试中，我们的测试策略通常是：

根据被测 API 输入参数的各种组合调用 API，并验证相关结果的正确性；

衡量上述测试过程的代码覆盖率；

根据代码覆盖率进一步找出遗漏的测试用例；

以代码覆盖率达标作为 API 测试成功完成的标志。

这也是单体架构时代主流的 API 测试策略。为了让你更好地理解这种测试策略，我来举一个实际的例子。

假设我们采用单体架构开发了一个系统，这个系统对外提供了 3 个 Restful API 接口，那么我们的测试策略应该是：

针对这 3 个 API 接口，分别基于边界值和等价类方法设计测试用例并执行；

在测试执行过程中，启用代码覆盖率统计；

假设测试完成后代码行覆盖率是 80%，那么我们就需要找到那些还没有被执行到的 20% 的代码行。比如图 2 中代码的第 242 行就是没有被执行到，分析代码逻辑后发现，我们需要构造“expected!=actual”才能覆盖这个未能执行的代码行；

最终我们要保证代码覆盖率达到既定的要求，比如行覆盖率达到 100%，完成 API 测试。

图 2 基于代码覆盖率指导测试用例设计的示例

而当我们采用微服务架构时，原本的单体应用会被拆分成多个独立模块，也就是很多个独立的 service，原本单体应用的全局功能将会由这些拆分得到的 API 共同协作完成。

比如，对于上面这个例子，没有微服务化之前，一共有 3 个 API 接口，假定现在采用微服务架构，该系统被拆分成了 10 个独立的 service，如果每个 service 平均对外暴露 3 个 API 接口，那么总共需要测试的 API 接口数量就多达 30 个。

如果我还按照传统的 API 测试策略来测试这些 API，那么测试用例的数量就会非常多，过多的测试用例往往就需要耗费大量的测试执行时间和资源。

但是，在互联网模式下，产品发布的周期往往是以“天”甚至是以“小时”为单位的，留给测试的执行时间非常有限，所以微服务化后 API 测试用例数量的显著增长就对测试发起了巨大的挑战。

这时，我们迫切需要找到一种既能保证 API 质量，又能减少测试用例数量的测试策略，这也就是我接下来要分享的基于消费者契约的 API 测试。

第二，微服务之间的耦合关系

微服务化后，服务与服务间的依赖也可能会给测试带来不小的挑战。

如图 3 所示，假定我们的被测对象是 Service T，但是 Service T 的内部又调用了 Service X 和 Service Y。此时，如果 Service X 和 Service Y 由于各种原因处于不可用的状态，那么此时就无法对 Service T 进行完整的测试。

图 3 API 之间的耦合示例

我们迫切需要一种方法可以将 Service T 的测试与 Service X 和 Service Y 解耦。

解耦的方式通常就是实现 Mock Service 来代替被依赖的真实 Service。实现这个 Mock Service 的关键点就是要能够模拟真实 Service 的 Request 和 Response。当我介绍完基于消费者契约的 API 测试后，你会发现这个问题也就迎刃而解了。

基于消费者契约的 API 测试

那到底什么是基于消费者契约的 API 测试呢？直接从概念的角度解释，会有些难以理解。所以我打算换个方法来帮助你从本质上真正理解什么是基于消费者契约的 API 测试。接下来，就跟着我的思路走吧。

首先，我们来看图 4，假设图 4 中的 Service A、Service B 和 Service T 是微服务拆分后的三个 Service，其中 Service T 是被测试对象，进一步假定 Service T 的消费者（也就是使用者）一共有两个，分别是 Service A 和 Service B。

图 4 Service A、Service B 和 Service T 的关系

按照传统的 API 测试策略，当我们需要测试 Service T 时，需要找到所有可能的参数组合依次对 Service T 进行调用，同时结合 Service T 的代码覆盖率进一步补充遗漏的测试用例。

这种思路本身没有任何问题，但是测试用例的数量会非常多。那我们就需要思考，如何既能保证 Service T 的质量，又不需要覆盖全部可能的测试用例。

静下心来想一下，你会发现 Service T 的使用者是确定的，只有 Service A 和 Service B，如果可以把 Service A 和 Service B 对 Service T 所有可能的调用方式都测试到，那么就一定可以保证 Service T 的质量。即使存在某些 Service T 的其他调用方式有出错的可能性，那也不会影响整个系统的功能，因为这个系统中并没有其他 Service 会以这种可能出错的方式来调用 Service T。

现在，问题就转化成了如何找到 Service A 和 Service B 对 Service T 所有可能的调用方式。如果能够找出这样的调用集合，并以此作为 Service T 的测试用例，那么只要这些测试用例 100% 通过，Service T 的质量也就不在话下了。

从本质上来讲，这样的测试用例集合其实就是，Service T 可以对外提供的服务的契约，所以我们把这个测试用例的集合称为“基于消费者契约的 API 测试”。

那么接下来，我们要解决的问题就是：如何才能找到 Service A 和 Service B 对 Service T 的所有可能调用了。其实这也很简单，在逻辑结构上，我们只要在 Service T 前放置一个代理，所有进出 Service T 的 Request 和 Response 都会经过这个代理，并被记录成 JSON 文件，也就构成了 Service T 的契约。

如图 5 所示，就是这个过程的原理了。

图 5 收集消费者契约的逻辑原理

在实际项目中，我们不可能在每个 Service 前去放置这样一个代理。但是，微服务架构中往往会存在一个叫作 API Gateway 的组件，用于记录所有 API 之间相互调用关系的日志，我们可以通过解析 API Gateway 的日志分析得到每个 Service 的契约。

至此，我们已经清楚地知道了如何获取 Service 的契约，并由此来构成 Service 的契约测试用例。接下来，就是如何解决微服务之间耦合关系带来的问题了。

微服务测试的依赖解耦和 Mock Service

在前面的内容中，我说过一句话：实现 Mock Service 的关键，就是要能够模拟被替代 Service 的 Request 和 Response。

此时我们已经拿到了契约，契约的本质就是 Request 和 Response 的组合，具体的表现形式往往是 JSON 文件，此时我们就可以用该契约的 JSON 文件作为 Mock Service 的依据，也就是在收到什么 Request 的时候应该回复什么 Response。

下面的图 6 就解释了这一关系，当用 Service X 的契约启动 Mock Service X 后，原本真实的 Service X 将被 Mock Service X 替代，也就解耦了服务之间的依赖，图 6 中的 Service Y 也是一样的道理。

图 6 基于 Mock Service 解决 API 之间的调用依赖

代码实例

自此，我已经讲完了基于消费者契约的 API 测试的原理，你是否已经都真正理解并掌握了呢？

由于这部分内容的理论知识比较多，为了帮你更好地理解这些概念，我找了一个基于 Spring Cloud Contract 的实际代码的示例演示契约文件格式、消费者契约测试以及微服务之间解耦，希望可以帮到你。

具体的实例代码，你可以从https://github.com/SpectoLabs/spring-cloud-contract-blog下载，详细的代码解读可以参考https://specto.io/blog/2016/11/16/spring-cloud-contract/。

这个实例代码，基于 Spring Boot 实现了两个微服务：订阅服务（subscription-service）和账户服务（account-service），其中订阅服务会调用账户服务。这个实例基于 Spring Cloud Contract，所以契约是通过 Groovy 语言描述的，也就是说实例中会通过 Groovy 语言描述的账户服务契约来模拟真实的账户服务。

这个实例的逻辑关系如图 7 所示。

图 7 基于 Spring Cloud Contract 的契约测试实例

总结

单体架构，具有灵活性差、可扩展性差、可维护性差等局限性，所以有了微服务架构。

微服务架构的本身的特点，比如微服务数量多，各个微服务之间的相互调用，决定了不能继续采用传统 API 测试的策略。

为了既能保证 API 质量，又能减少测试用例数量，于是有了基于消费者契约的 API 测试。基于消费者契约的 API 测试的核心思想是：只测试那些真正被实际使用到的 API 调用，如果没有被使用到的，就不去测试。

基于消费者契约的测试方法，由于收集到了完整的契约，所以基于契约的 Mock Service 完美地解决了 API 之间相互依赖耦合的问题。

这已经是 API 自动化测试系列的最后一篇文章了，短短的三篇文章可能让你感觉意犹未尽，也可能感觉并没有涵盖到你在实际工程项目中遇到的 API 测试的所有问题，但是一个专栏区区几十篇文章的确无法面面俱到。

我通过这个专栏更想达到的目的是：讲清楚某一技术的来龙去脉及其应用场景，但是很多具体操作级别、代码实现级别的内容，还是需要你在实践中不断积累。

所以，如果你还有关于 API 测试的其他问题，非常欢迎你给我留言讨论，让我们一起来碰撞出思想火花吧！

思考题

基于消费者契约的 API 测试中，对于那些新开发的 API，或者加了新功能的 API，由于之前都没有实际的消费者，所以你无法通过 API Gateway 方法得到契约。对于这种情况，你会采用什么方法来解决呢？

欢迎你给我留言。

25 | 不破不立：掌握代码级测试的基本理念与方法

你好，我是茹炳晟，今天我和你分享的主题是“不破不立：掌握代码级测试的基本理念与方法”。

我在第三篇文章《什么是单元测试？如何做好单元测试？》中，为你介绍了单元测试的基本概念和方法，和你聊到了单元测试用例的“输入数据”和“预计输出”，也谈到了驱动代码和桩代码，其实这些概念和方法在代码级测试中也是最基本的。

通常情况下，代码级测试的工作都是由开发人员完成，但是测试框架选型、覆盖率统计工具选型、测试用例设计原则等都需要资深的测试工程师或者测试架构师参与。

所以，代码级测试这个系列，我会和你分享测试人员应该具备的代码级测试基础知识，为你呈现一幅包括代码级测试技术入门、方法论、用例设计，以及覆盖率衡量、典型难点、解决思路的全景技术视图。

为了能更好地协助开发人员做好代码级测试，所以我今天的这次分享是根据实际工程项目中的实践，总结了五种常见的代码错误，以及对应的四大类代码级测试方法。

掌握了这些错误类型、测试方法，相信你就可以搞定代码级测试了，即使自己不用去完成测试工作，也可以让开发人员对你另眼相看，可以更高效地互相配合完成整个项目。

这里需要注意的是，代码级测试的测试方法一定是一套测试方法的集合，而不是一个测试方法。 因为单靠一种测试方法不可能发现所有潜在的错误，一定是一种方法解决一部分或者一类问题，然后综合运用多种方法解决全部问题。

本着先发现问题，然后解决问题的思路，我在正式介绍代码级测试方法之前，先来概括一下常见的代码错误类型，然后我们再一起讨论代码级测试有哪些方法。这样，我们就可以清晰地看出，每一种代码级测试方法都能覆盖哪些类型的代码错误。

根据过往的经验来看，代码错误，可以分为“有特征”的错误和“无特征”的错误两大类。“有特征”的错误，可进一步分为语法特征错误、边界行为错误和经验特征错误；“无特征”的错误，主要包括算法错误和部分算法错误。

接下来，我将和你详细说说这五类代码错误的具体含义是什么。

常见代码错误类型

第一，语法特征错误

语法特征错误是指，从编程语法上就能发现的错误。比如，不符合编程语言语法的语句等。

如果你使用 IDE 环境进行代码开发，那么 IDE 可以提示你大部分的这类错误，而且只有解决了这类错误，才能编译通过。但是，还会有一些比较隐晦的语法特征错误，IDE 不能及时发现，而且也不会影响编译，只会在运行阶段出错。

void demoMethod(void)

{

  int a[10];

  a[10]=88;

  ...

}

比如，这段 C 语言代码就存在数据越界的问题。

很显然，你从语法上很容易就能发现，这段代码初始化了一个长度为 10 的整型数组 a，但数组下标从 0 开始，所以最大可用的数组空间应该是 a[9]，而这里却使用了 a[10]，造成数组越界，访问了未被初始化的内存空间，代码运行时（Runtime）就会造成意想不到的结果。

第二，边界行为特征错误

边界行为特征错误是指，代码在执行过程中发生异常，崩溃或者超时。之所以称为“边界”，是由于此类错误通常都是发生在一些边界条件上。

int Division(int a, int b)

{

  return a/b;

}

这段 C 语言代码就存在具有边界行为特征的错误。当 b 取值为 0 时，Division 函数就会抛出运行时异常。

第三，经验特征错误

经验特征错误是指，根据过往经验发现代码错误。

void someMethod(void)

{

  ...

  if(i=2)

  {

    // if the value of i equals to 2, call method "operationA"

    operationA();

  }

  else

  {

    // if the value of i doesn't equal to 2, call method "operationB"

    operationB();

  }

}

这段 C 语言代码，就是一个典型的具有经验特征错误的代码片段。代码想要表达的意思是：如果变量 i 的值等于 2，就调用函数 operationA；否则，调用函数 operationB。

但是，代码中将“if(i==2)”错误地写成了“if(i=2)”，就会使原本的逻辑判断操作变成了变量赋值操作，而且这个赋值操作的返回结果永远是 true，即这段代码永远只会调用 operationA 的分支。

显然，“if(i=2)”在语法上没有错误，但是从过往经验来看，这就很可能是个错误了。也就是说，当你发现一个原本应该出现逻辑判断语句的地方，现在却出现了赋值语句，那就很有可能是代码写错了。

第四，算法错误

算法错误是指，代码完成的计算（或者功能）和之前预先设计的计算结果（或者功能）不一致。

这类错误直接关系到代码需要实现的业务逻辑，在整个代码级测试中所占比重最大，也是最重要的。但是，完全的算法错误并不常见，因为不能准确完成基本功能需求的代码，是一定不会被递交的。所以，在实际工程项目中，最常见的是部分算法错误。

第五，部分算法错误

部分算法错误是指，在一些特定的条件或者输入情况下，算法不能准确完成业务要求实现的功能。这类错误，是整个代码级测试过程中最常见的类型。

int add(int a, int b)

{

  return a+b;

}

这段 C 语言代码，完成了两个 int 类型整数的加法运算。在大多数情况下，这段代码的功能逻辑都是正确的，能够准确地返回两个整数的加法之和。但是，在某些情况下，可能存在两个很大的整数相加后和越界的情况，也就是说两个很大的 int 数相加的结果超过了 int 的范围。这就是典型的部分算法错误。

代码级测试常用方法

介绍完了语法特征错误、边界行为特征错误、经验特征错误、算法错误、部分算法错误这五类代码错误后，我们再回过头来看看代码级测试的方法有哪些，这些测试方法又是如何揭露这五类代码错误的。

在我看来，代码级测试方法主要分为两大类，分别是静态方法和动态方法。

静态方法，顾名思义就是在不实际执行代码的基础上发现代码缺陷的方法，又可以进一步细分为人工静态方法和自动静态方法；

动态方法是指，通过实际执行代码发现代码中潜在缺陷的方法，同样可以进一步细分为人工动态方法和自动动态方法。

这里需要注意到的是，我在这篇文章中只会和你分享这四种方法具体是什么，各有何局限性和优势，分别可以覆盖哪些错误类型。而对于，具体如何用这四种方法完成代码级测试，测试用例如何设计、常用的测试工具如何使用，我会在后面两篇文章（《深入浅出之静态测试方法》和《深入浅出之动态测试方法》）中详细展开。

第一，人工静态方法

人工静态方法是指，通过人工阅读代码查找代码中潜在错误的方法，通常采用的手段包括，开发人员代码走查、结对编程、同行评审等。

理论上，人工静态方法可以发现上述五类代码错误，但实际效果却并不理想。 这个方法的局限性，主要体现在以下三个方面：

过度依赖于代码评审者的个人能力，同样的评审流程，发现的问题却相差悬殊；

如果开发人员自行走查自己的代码，往往会存在“思维惯性”，开发过程中没有能考虑的输入和边界值，代码走查时也一样会被遗漏；

由于完全依赖人工，效率普遍较低。

第二，自动静态方法

自动静态方法是指，在不运行代码的方式下，通过词法分析、语法分析、控制流分析等技术，并结合各种预定义和自定义的代码规则，对程序代码进行静态扫描发现语法错误、潜在语义错误，以及部分动态错误的一种代码分析技术。

自动静态方法可以发现语法特征错误、边界行为特征错误和经验特征错误这三类“有特征”的错误，但对于算法错误和部分算法错误这两种“无特征”的错误却无能为力。根本原因在于，自动静态方法并不清楚代码的具体业务逻辑。

目前，自动静态方法无论是在传统软件企业，还是在互联网软件企业都已经被广泛采用，往往会结合企业或项目的编码规范一起使用，并与持续集成过程紧密绑定。

你需要根据不同的开发语言，选择不同的工具。目前有很多工具都可以支持多种语言，比如 Sonar、Coverity 等，你可以根据实际需求来选择。

第三，人工动态方法

人工动态方法是指，设计代码的输入和预期的正确输出的集合，然后执行代码，判断实际输出是否符合预期。我在之前的第三篇文章《什么是单元测试？如何做好单元测试？》中介绍的单元测试，采用的测试方法本质上就是人工动态方法。

在代码级测试中，人工动态方法是最主要的测试手段，可以真正检测代码的逻辑功能，其关注点是“什么样的输入，执行了什么代码，产生了什么样的输出”，所以最善于发现算法错误和部分算法错误。

目前，不同的编程语言对应有不同的单元测试框架，比如，对 Java 语言最典型的是 Junit 和 TestNG，对于 C 语言比较常用的是 Google Test 等。

第四，自动动态方法

自动动态方法，又称自动边界测试方法，指的是基于代码自动生成边界测试用例并执行，以捕捉潜在的异常、崩溃和超时的方法。

自动动态方法，可以覆盖边界行为特征错误， 通常能够发现“忘记处理某些输入”引起的错误（因为容易忘记处理的输入，往往是“边界”输入）。但是它对于发现算法错误无能为力，毕竟工具不可能了解代码所要实现的功能逻辑。

总结

作为代码级测试系列的第一篇文章，我今天主要和你分享了代码级测试中的常见代码错误类型，以及常用测试方法。

代码错误，可以划分为“有特征”的错误和“无特征”的错误两大类。其中，“有特征”的错误，又可以进一步细分为语法特征错误、边界行为特征错误和经验特征错误；而“无特征”的错误，主要包括算法错误和部分算法错误两类。

针对这五种代码错误，我将代码级测试的方法分成了静态方法和动态方法两大类。顾名思义，静态方法不需要执行实际代码，而动态方法需要通过执行具体的代码去发现代码错误。而每一类方法又可以根据执行方式，进一步细分。也因此，每种测试方法，所能覆盖的错误类型也不同，所以进行代码级测试时，你需要综合运用这些方法，并结合所在公司或者项目的编码规范一起使用。

这四类测试方法的特点，以及可以覆盖的错误类型，可以概括如下：

人工静态方法，本质上通过开发人员代码走查、结对编程、同行评审来完成的，理论上可以发现所有的代码错误，但也因为其对“测试人员”的过渡依赖，局限性非常大；

自动静态方法，主要的手段是代码静态扫描，可以发现语法特征错误、边界行为特征错误和经验特征错误这三类“有特征”的错误；

人工动态方法，就是传统意义上的单元测试，是发现算法错误和部分算法错误的最佳方式；

自动动态方法，其实就是自动化的边界测试，主要覆盖边界行为特征错误。

思考题

你所在的公司，还采用过哪些代码级测试的方法，你们又是如何具体开展的呢？

欢迎你给我留言。

26 | 深入浅出之静态测试方法

你好，我是茹炳晟，今天我和你分享的主题是：深入浅出之静态测试方法。

我在分享《不破不立：掌握代码级测试的基本理念与方法》这个主题时，系统地介绍了代码级测试常见的五种错误类型（包括语法特征错误、边界行为特征错误、经验特征错误、算法错误，以及部分算法错误），以及对应的四大类测试方法（包括人工静态方法、自动静态方法、人工动态方法，以及自动动态方法）。

今天，我将和你详细讨论人工静态测试方法和自动静态测试方法，来帮你理解研发流程上是如何保证代码质量的，以及如何搭建自己的自动静态代码扫描方案，并且应用到项目的日常开发工作中去。

人工静态方法本质上属于流程上的实践，实际能够发现问题的数量很大程度依赖于个人的能力，所以从技术上来讲这部分内容可以讨论的点并不多。但是，这种方法已经在目前的企业级测试项目中被广泛地应用了，所以我们还是需要理解这其中的流程，才能更好地参与到人工静态测试中。

而自动静态方法，可以通过自动化的手段，以很低的成本发现并报告各种潜在的代码质量问题，目前已经被很多企业和项目广泛采用，并且已经集成到 CI/CD 流水线了。作为测试工程师，我们需要完成代码静态扫描环境的搭建。接下来我会重点和你分享这一部分内容。

人工静态方法

通过我上一次的分析，我们知道了人工静态方法检查代码错误，主要有代码走查、结对编程，以及同行评审这三种手段。那么我们接下来就看一下这三种方法是如何执行的。

代码走查（Code Review），是由开发人员检查自己的代码，尽可能多地发现各类潜在错误。但是，由于个人能力的差异，以及开发人员的“思维惯性”，很多错误并不能在这个阶段被及时发现。

结对编程（Pair Programming），是一种敏捷软件开发的方法，一般是由两个开发人员结成对子在一台计算机上共同完成开发任务。其中，一个开发人员实现代码，通过被称为“驾驶员”；另一个开发人员审查输入的每一行代码，通常被称为“观察员”。
当“观察员”对代码有任何疑问时，会立即要求“驾驶员”给出解释。解释过程中，“驾驶员”会意识到问题所在，进而修正代码设计和实现。
实际执行过程中，这两个开发人员的角色会定期更换。

同行评审（Peer Review），是指把代码递交到代码仓库，或者合并代码分支（Branch）到主干（Master）前，需要和你同技术级别或者更高技术级别的一个或多个同事对你的代码进行评审，只有通过所有评审后，你的代码才会被真正递交。
如果你所在的项目使用 GitHub 管理代码，并采用 GitFlow 的分支管理策略，那么在递交代码或者分支合并时，需要先递交 Pull Request（PR），只有这个 PR 经过了所有评审者的审核，才能被合并。这也是同行评审的具体实践。目前，只要你采用 GitFlow 的分支管理策略，基本都会采用这个方式。

对于以上三种方式，使用最普遍的是同行评审。因为同行评审既能较好地保证代码质量，又不需要过多的人工成本投入，而且递交的代码出现问题后责任明确，另外代码的可追溯性也很好。

结对编程的实际效果虽然不错，但是对人员的利用率比较低，通常被用于一些非常关键和底层算法的代码实现。

自动静态方法

自动静态方法，主要有以下三个特点：

相比于编译器，可以做到对代码更加严格、个性化的检查；

不真正检测代码的逻辑功能，只是站在代码本身的视角，基于规则，尽可能多地去发现代码错误；

由于静态分析算法并不实际执行代码，完全是基于代码的词法分析、语法分析、控制流分析等技术，由于分析技术的局限性以及代码写法的多样性，所以会存在一定的误报率。

基于这些特点，自动静态方法通常能够以极低的成本发现以下问题：

使用未初始化的变量；

变量在使用前未定义；

变量声明了但未使用；

变量类型不匹配；

部分的内存泄漏问题；

空指针引用；

缓冲区溢出；

数组越界；

不可达的僵尸代码；

过高的代码复杂度；

死循环；

大量的重复代码块；

…

正是由于自动静态方法具有自动化程度高，检查发现问题的成本低以及能够发现的代码问题广等特点，所以该方法被很多企业和项目广泛应用于前期代码质量控制和代码质量度量。

在实际工程实践中，企业往往会结合自己的编码规范定制规程库，并与本地 IDE 开发环境和持续集成的流水线进行高度整合。

代码本地开发阶段，IDE 环境就可以自动对代码实现自动静态检查；当代码递交到代码仓库后，CI/CD 流水线也会自动触发代码静态检查，如果检测到潜在错误，就会自动邮件通知代码递交者。

接下来，我们一起来看两个自动静态方法发现错误的实际案例，希望可以加深你对自动静态方法的认识。

自动静态方法的实际例子

第一个例子，自动静态方法检查语法特征错误。

如图 1 左侧所示的 C 语言代码，存在数组越界的问题，一种典型的语法特征错误。

图 1 右侧，就是通过 C 语言的自动静态扫描工具 splint 发现的这个问题，并给出的分析结果。

图 1 数组越界的错误

第二个例子，自动静态方法检查内存空间被释放后继续被赋值的错误。

如图 2 左侧所示的 C 语言代码，我们用 malloc 函数申请了一个内存空间，并用指针 a 指向了这个空间，然后新建了一个指针 b 也指向这个空间，也就是指针 a 和指针 b 实际上指向了同一个内存空间。之后，我们把指针 a 指向的空间释放掉了，意味着指针 b 指向的空间也被释放了。但是，此时代码却试图去对指针 b 指向的空间赋值，显然这会导致不可预料的后果。

幸运的是，C 语言的自动静态扫描工具 splint 发现了这个问题，并给出了详细解释。

图 2 内存空间释放后还继续使用的错误

实际案例：Sonar 实战

现在，我们已经了解了自动静态代码扫描的基本概念，那怎么把这些知识落地到你的实际项目中呢？我们就从目前主流的自动静态工具 Sonar 的使用开始吧。

考虑到你可能以前并没有接触过 Sonar，所以我会按照 step by step 的节奏展开。如果你已经用过 Sonar 了，你可以跳过在 Mac 电脑上建立 Sonar 的步骤，从完成你的 Maven 项目的自动静态分析开始。

通过这个 Sonar 实例，你可以掌握：

搭建自己的 SonarQube 服务器；

扫描 Maven 项目，并将结果报告递交到 SonarQube 服务器；

在 IntelliJ IDE 中集成 SonarLint 插件，在 IDE 中实现实时的自动静态分析；

首先，在Sonar 官网下载 LTS（Long-term Support）版本的 SonarQube 6.7.5。这里需要注意的是，我不推荐在实际工程项目中使用最新版的 SonarQube，而是建议使用 LTS 版本以保证稳定性和兼容性。

解压后运行其中的 bin/macosx-universal-64 目录下的 sonar.sh，这里需要注意运行 sonar.sh 时要带上“console”参数。如果执行完成的界面如下图 3 所示，那么说明你的 SonarQube 服务已经成功启动。

图 3 SonarQube 启动成功的界面

此时，你可以尝试访问 localhost:9000，并用默认账号（用户名和密码都是“admin”）登录。

为了简化建立 SonarQube 的步骤，所有的内容我都使用了默认值。比如，我直接使用了 SonarQube 内建的数据库，端口也采用了默认的 9000。但是，在实际工程项目中，为了 Sonar 数据的长期可维护和升级，我们通常会使用自己的数据库，需要执行下面这些步骤：

安装 SonarQube 之前，先安装数据库；

建立一个空数据库并赋予 CRUD 权限；

修改 SonarQube 的 conf/sonar.properties 中的 JDBC 配置，使其指向我们新建的数据库。我们也可以采用同样的方法，来修改默认的端口。

因为要在 Maven 项目中执行代码静态扫描，为此我们需要先找到 $MAVEN_HOME/conf 下的 settings.xml 文件，在文件中加入 Sonar 相关的全局配置，具体需要加入的内容如下所示：

<settings>

    <pluginGroups>

        <pluginGroup>org.sonarsource.scanner.maven</pluginGroup>

    </pluginGroups>

    <profiles>

        <profile>

            <id>sonar</id>

            <activation>

                <activeByDefault>true</activeByDefault>

            </activation>

            <properties>

                <sonar.host.url>

                  http://myserver:9000

                </sonar.host.url>

            </properties>

        </profile>

     </profiles>

</settings>

最后，我们就可以在 Maven 项目中，执行“mvn clean verify sonar:sonar”命令完成静态代码扫描。

如果你是第一次使用这个命令，那么 mvn 会自动下载依赖 maven-sonar-plugin，完成后发起代码的静态扫描，并会自动把扫描结果显示到 SonarQube 中。

图 4 所示的结果，就是我对《从 0 到 1：你的第一个 GUI 自动化测试》一文中的 GUI 测试项目代码的扫描结果。

图 4 SonarQube 的静态扫描结果页面

扫描结果是 Passd，但同时也发现了三个 Code Smell 问题，或者说是改进建议，如图 5 所示。

Class 建议放在 package 中；

导入了 java.io.BufferedInputStream，但没有在实际代码中使用，建议删除；

建议变量名字不要包含下划线。

图 5 详细扫描结果示例

至此，你已经使用 Sonar 完成了一次代码的静态扫描，是不是还挺方便的？

但是，在日常工作中你可能还想要实时看到 Sonar 分析的结果，这样可以大幅提高修改代码的效率。为此，我们可以在 IDE 中引入 SonarLint 插件。你可以通过 IDE 的 plugin（插件）管理界面安装 SonarLint。

安装完成后重启 IDE，你就可以在 IDE 环境中实时看到 Sonar 的静态分析结果了，如图 6 所示。

图 6 在 IDE 中直接查看静态扫描结果

另外，在 IDE 中绑定 SonarQube，就可以把 SonarLint 和 SonarQube 集成在一起了，如图 7 所示。集成完成后，IDE 本地的代码扫描就能使用 SonarQube 端的静态代码规则库了，在企业级的项目中，一般要求所有开发人员都使用统一的静态代码规则库，所以一般都会要求本地 IDE 的 SonarLint 与 SonarQube 集成。

图 7 IDE 中的 SonarLint 和 SonarQube 绑定

目前，自动静态扫描通常都会和持续集成的流水线做绑定，最常见的应用场景是当你递交代码后，持续集成流水线就会自动触发自动静态扫描，这一功能是通过 Jenkins 以及 Jenkins 上的 SonarQube 插件来完成的，当你在 Jenkins 中安装了 SonarQube Plugin，并且将 SonarQube 服务器相关的配置信息加入 Plugin 之后，你就可以在 Jenkins Job 的配置中增加 Sonar 静态扫描步骤了。

总结

人工静态方法，主要有代码走查、结对编程和同行评审三种常用方法。在工程实践中，同行评审因为可以保证代码质量、效率高、责任明确等特点，已经被广泛采用。

自动静态方法，因为自动化程度高、成本低、发现的代码问题广等特点，是常用的代码级测试方法。

在这里，测试工程师需要完成代码静态扫描环境的搭建，考虑到你以前可能没有接触过 Sonar，我按照 step by step 的思路，带你一起搭建了一套代码静态扫描环境，并分享了一个 Maven 项目代码静态扫描的实例。

这就是我今天分享的主要内容了，希望可以帮助你解决实际工作中遇到的问题。

思考题

除了 Sonar，你还用过哪些静态代码扫描工具，使用过程中遇到过哪些问题？

欢迎你给我留言。

27 | 深入浅出之动态测试方法

你好，我是茹炳晟，今天我和你分享的主题是：深入浅出之动态测试方法。

相较于，静态测试方法是不需要实际执行代码去发现潜在代码错误的方法，我今天要和你讨论的动态测试方法，则是要通过实际执行代码去发现潜在代码错误的测试方法。

正如我在分享《不破不立：掌握代码级测试的基本理念与方法》这个主题时，将动态测试方法进一步划分为人工动态方法和自动动态方法，今天这次关于动态测试方法的分享，我也会从这两个方面展开。

由于自动动态方法并不能理解代码逻辑，所以仅仅被用于发现异常、崩溃和超时这类“有特征”的错误，而对于代码逻辑功能的测试，主要还是要依靠人工动态方法。

人工动态方法

人工动态方法，可以真正检测代码的业务逻辑功能，其关注点是“什么样的输入，执行了什么代码，产生了什么样的输出”，主要用于发现算法错误和部分算法错误，是最主要的代码级测试手段。

从人工动态方法的定义中，你可以很清楚地看出：代码级测试的人工动态测试方法，其实就是单元测试所采用的方法。所以，下面的分享，我会从单元测试方法的角度展开。

如果有一些代码基础，那么你在学习单元测试框架或者工具时，会感觉单元测试很简单啊，一点都不难：无非就是用驱动代码去调用被测函数，并根据代码的功能逻辑选择必要的输入数据的组合，然后验证执行被测函数后得到的结果是否符合预期。 但是，一旦要在实际项目中开展单元测试时，你会发现有很多实际的问题需要解决。

我在专栏第 4 篇文章《什么是单元测试？如何做好单元测试？》中，已经分享过单元测试中的主要概念了，所以今天的分享我不会重复前面的内容，只和你分享前面没有涉及到的部分。如果你有哪些概念已经记不太清楚了，建议你先回顾一下那篇文章的内容。

接下来，我将和你分享单元测试中三个最主要的难点：

单元测试用例“输入参数”的复杂性；

单元测试用例“预期输出”的复杂性；

关联依赖的代码不可用。

单元测试用例“输入参数”的复杂性

提到“输入参数”的复杂性，你应该已经记起了，我在前面的分享中提到过：如果你认为单元测试的输入参数只有被测函数的输入参数的话，那你就把事情想得过于简单了。

其实，这也是源于我们在学习单元测试框架时，单元测试用例的输入数据一般都是被测函数的输入参数，所以我们的第一印象会觉得单元测试其实很简单。

但是到了实际项目时，你会发现单元测试太复杂了，因为测试用例设计时需要考虑的“输入参数”已经完全超乎想象了。

我在《什么是单元测试？如何做好单元测试？》一文中已经总结了多种常见的单元测试输入数据，但是并没有详细解释每种输入数据的具体含义，你可能也对此感到困惑，那么今天我就结合一些代码示例和你详细聊聊这些输入参数吧。

第一，被测试函数的输入参数

这是最典型，也是最好理解的单元测试输入数据类型。假如你的被测函数是下面这段代码中的形式，那么函数输入参数 a 和 b 的不同取值以及取值的组合就构成了单元测试的输入数据。

int someFunc(int a, int b)

{

  …

}

第二，被测试函数内部需要读取的全局静态变量

如果被测函数内部使用了该函数作用域以外的变量，那么这个变量也是被测函数的输入参数。

下面这段代码中，被测函数 Func_SUT 的内部实现中使用了全局变量 someGlobalVariable，并且会根据 someGlobalVariable 的取值去执行 FuncA() 和 FuncB() 这不同的代码分支。

在做单元测试时，为了能够覆盖这两个分支，你就必须构造 someGlobalVariable 的不同取值，那么自然而然，这个 someGlobalVariable 就成为了被测函数的输入参数。

所以，在这段代码中，单元测试的输入参数不仅包括 Func_SUT 函数的输入参数 a，还包括全局变量 someGlobalVariable。

bool someGlobalVariable = true；

void Func_SUT(int a)

{

  ...

  if(someGlobalVariable == true)

  {

    FuncA();

  }

  else

  {

    FuncB();

  }

  ...

}

第三，被测试函数内部需要读取的类成员变量

如果你能理解“被测函数内部需要读取的全局静态变量”是单元测试的输入参数，那么“被测试函数内部需要读取的类成员变量”也是单元测试的输入参数就不难理解了。因为，类成员变量对被测试函数来讲，也可以看做是全局变量。

我们一起看一段代码。这段代码中，变量 someClassVariable 是类 someClass 的成员变量，类的成员函数 Func_SUT 是被测函数。Func_SUT 函数，根据 someClassVariable 的取值不同，会执行两个不同的代码分支。

同样地，单元测试想要覆盖这两个分支，就必须提供 someClassVariable 的不同取值，所以 someClassVariable 对于被测函数 Func_SUT 来说也是输入参数。

class someClass{

  ...

  bool someClassVariable = true；

  ...

  void Func_SUT(int a)

  {

    ...

    if(someClassVariable == true)

    {

      FuncA();

    }

    else

    {

      FuncB();

    }

    ...

  }

  ...

}

第四，函数内部调用子函数获得的数据

“函数内部调用子函数获得的数据”也是单元测试的输入数据，从字面上可能不太好理解，那我就通过一段代码，和你详细说说这是怎么回事吧。

void Func_SUT(int a)

  {

  bool toggle = FuncX(a);

  if(toggle == true)

  {

    FuncA();

  }

  else

  {

    FuncB();

  }

}

函数 Func_SUT 是被测函数，它的内部调用了函数 FuncX，函数 FuncX 的返回值是 bool 类型，并且赋值给了内部变量 toggle，之后的代码会根据变量 toggle 的取值来决定执行哪个代码分支。

那么，从输入数据的角度来看，函数 FuncX 的调用为被测函数 Func_SUT 提供了数据，也就是这里的变量 toggle，后续代码逻辑会根据变量 toggle 的取值执行不同的分支。所以，从这个角度来看，被测函数内部调用子函数获得的数据也是单元测试的输入参数。

这里还有一个小细节，被测函数 Func_SUT 的输入参数 a，在内部实现上只是传递给了内部调用的函数 FuncX，而并没有在其他地方被使用，我们把这类用于传递给子函数的输入参数称为“间接输入参数”。

这里需要注意的是，有些情况下“间接输入参数”反而不是输入参数。

就以这段代码为例，如果我们发现通过变量 a 的取值很难控制 FuncX 的返回值（也就是说，当通过间接输入参数的取值去控制内部调用函数的取值，以达到控制代码内部执行路径比较困难）时，我们会直接对 FuncX(a) 打桩，用桩代码来控制函数 FuncX 返回的是 true 还是 false。

这样一来，原本的变量 a 其实就没有任何作用了。那么，此时变量 a 虽然是被测函数的输入参数，但却并不是单元测试的输入参数。

第五，函数内部调用子函数改写的数据

理解了前面几种单元测试的输入参数类型后，“函数内部调用子函数改写的数据”也是单元测试中被测函数的输入参数就好解释了。

比如，当被测函数内部调用的子函数改写了全局变量或者类的成员变量，而这个被改写的全局变量或者类的成员变量又会在被测函数内部被使用，那么“函数内部调用子函数改写的数据”也就成为了被测函数的输入参数了。

第六，嵌入式系统中，在中断调用中改写的数据

嵌入式系统中，在中断调用中改写的数据有时候也会成为被测函数的输入参数，这和“函数内部调用子函数改写的数据也是单元测试中的输入参数”类似，在某些中断事件发生并执行中断函数时，中断函数很可能会改写某个寄存器的值，但是被测函数的后续代码还要基于这个寄存器的值进行分支判断，那么这个被中断调用改写的数据也就成了被测函数的输入参数。

其实在实际工程项目中，除了这六种输入参数，还有很多输入参数。在这里，我详细分析这六种输入参数的目的，一来是帮你理解到底什么样的数据是单元测试的输入数据，二来也是希望你可以从本质上认识单元测试的输入参数，那么在以后遇到相关问题时，你也可以做到触类旁通，不会再踌躇无措。

理解了“输入参数”的复杂性，接下来我们再一起看看“预期输出”的复杂性表现在哪些方面。

单元测试用例“预期输出”的复杂性

同样地，单元测试用例的“预期输出”，也绝对不仅仅是函数返回值这么简单。通常来讲，“预期输出”应该包括被测函数执行完成后所改写的所有数据，主要包括：被测函数的返回值，被测函数的输出参数，被测函数所改写的成员变量和全局变量，被测函数中进行的文件更新、数据库更新、消息队列更新等。

第一，被测函数的返回值

这是最直观的预期输出。比如，加法函数 int add(int a, int a) 的返回值就是预期输出。

第二，被测函数的输出参数

要理解“被测函数的输出参数”是预期输出，最关键的是要理解什么是函数的输出参数。如果你有 C 语言背景，那么你很容易就可以理解这个概念了。

我们一起来看一段代码。被测函数 add 包含三个参数，其中 a 和 b 是输入参数，而 sum 是个指针，指向了一个地址空间。

如果被测函数的代码对 sum 指向的空间进行了赋值操作，那么在被测函数外，你可以通过访问 sum 指向的空间来获得被测函数内所赋的值，相当于你把函数内部的值输出到了函数外，所以 sum 对于函数 add 来讲其实是用于输出加法结果的，那么显然这个 sum 就是我们的“预期输出”。

如果你还没有理解的话，可以在百度上搜索一下“C 语言的参数传递机制”。

void add(int a, int b，int *sum)

{

  *sum = a + b;

}

void main()

{

  int a, b，sum;

  a = 10;

  b = 8;

  add(a, b, &sum);

  printf("sum = %d \n", sum);

}

第三，被测函数所改写的成员变量和全局变量

理解了单元测试用例“输入参数”的复杂性，“被测函数所改写的成员变量和全局变量”也是被测函数的“预期输出”就很好理解了，此时如果你的单元测试用例需要写断言来验证结果，那么这些被改写的成员变量和全局变量就是 assert 的对象。

第四，被测函数中进行的文件更新、数据库更新、消息队列更新等

这应该不难理解。

但在实际的单元测试实践中，因为测试解耦的需要，所以一般不会真正去做这些操作，而是借助对 Mock 对象的断言来验证是否发起了相关的操作。

关联依赖的代码不可用

什么是关联依赖的代码呢？

假设被测函数中调用了其他的函数，那么这些被调用的其他函数就是被测函数的关联依赖代码。

大型的软件项目通常是并行开发的，所以经常会出现被测函数关联依赖的代码未完成或者未测试的情况，也就是出现关联依赖的代码不可用的情况。那么，为了不影响被测函数的测试，我们往往会采用桩代码来模拟不可用的代码，并通过打桩补齐未定义部分。

具体来讲，假定函数 A 调用了函数 B，而函数 B 由其他开发团队编写，且未实现，那么我们就可以用桩函数来代替函数 B，使函数 A 能够编译链接，并运行测试。

桩函数要具有与原函数完全相同的原形，仅仅是内部实现不同，这样测试代码才能正确链接到桩函数。一般来讲桩函数主要有两个作用，一个是隔离和补齐，另一个是实现被测函数的逻辑控制。

用于实现隔离和补齐的桩函数实现比较简单，只需拷贝原函数的声明，加一个空的实现，可以通过编译链接就可以了。

用于实现控制功能的桩函数是最常用的，实现起来也比较复杂，需要根据测试用例的需要，输出合适的数据作为被测函数的内部输入。

自动动态方法

我们先来回顾一下，什么是自动动态方法。自动动态方法是，基于代码自动生成边界测试用例并执行来捕捉潜在的异常、崩溃和超时的测试方法。

自动动态方法的重点是：如何实现边界测试用例的自动生成。

解决这个问题最简单直接的方法是，根据被测函数的输入参数生成可能的边界值。

具体来讲，任何数据类型都有自己的典型值和边界值，我们可以预先为它们设定好典型值和边界值，然后组合就可以生成了。

比如，函数 int func(int a, char *s)，就可以按下面的三步来生成测试用例集。

定义各种数据类型的典型值和边界值。 比如，int 类型可以定义一些值，如 int 的最小值、int 的最大值、0、1、-1 等；char* 类型也可以定义一些值，比如“”、“abcde”、“非英文字符串”等。

根据被测函数的原形，生成测试用例代码模板，比如下面这段伪代码：

try{

  int a= @a@;

  char *s = @s@;

  int ret = func(a, s);

}

catch{

  throw exception();

}

将参数 @a@和 @s@的各种取值循环组合，分别替换模板中的相应内容，即可生成用例集。

由于该方法不可能自动了解代码所要实现的功能逻辑，所以不会验证“预期输出”，而是通过 try…catch 来观察是否会引发代码的异常、崩溃和超时等具有边界特征的错误。

总结

代码级测试的动态测试方法，可以分为人工动态测试方法和自动动态测试方法。其中人工动态测试方式，是最常用的代码级测试方法，也是我们在进行单元测试时采用的方法。

人工动态方法，也就是单元测试方法，通常看似简单，但在实际的工程实践中会遇到很多困难，总结来看这些困难可以概括为三大方面：

单元测试用例“输入参数”的复杂性，表现在“输入参数”不是简单的函数输入参数。本质上讲，任何能够影响代码执行路径的参数，都是被测函数的输入参数。

单元测试用例“预期输出”的复杂性，主要表现在“预期输出”应该包括被测函数执行完成后所改写的所有数据。

关联依赖的代码不可用，需要我们采用桩代码来模拟不可用的代码，并通过打桩补齐未定义部分。

而自动动态方法，需要重点讨论的是：如何实现边界测试用例的自动生成。解决这个问题最简单直接的方法是，根据被测函数的输入参数生成可能的边界值。

思考题

除了我们一起讨论的这些单元测试的难点，还有复杂数据初始化、函数内部不可控子函数的调用、间接输入参数的估算等难点。你在单元测试中是否遇到过这些问题呢，又是如何解决的？

感谢你的收听，欢迎给我留言一起讨论。

28 | 带你一起解读不同视角的软件性能与性能指标

你好，我是茹炳晟。今天我和你分享的主题是：带你一起解读不同视角的软件性能与性能指标。

我用三篇文章的篇幅，和你分享了代码级测试的一些基本概念和测试方法，希望可以帮助你解决在实际工作中遇到的问题，如果你感觉还有一些问题没有解决的话，欢迎你给我留言，我们一起去讨论、解决。

从今天开始，我将和你分享一个新的测试主题：性能测试。这个系列，我准备了七个主题，要和你详细聊聊软件性能测试相关的知识点。

因为性能测试的专业性很强，所以我会以从 0 到 1 的入门者视角，系统性地阐述性能测试的方法以及应用领域，用实例去诠释各种性能指标；我还会分享一些前端和后端性能测试工具的基本原理，并基于 LoadRunner 去分析一些大型企业性能测试的规划、设计、实现的具体实例；我还会和你分享一些跨国软件公司性能测试卓越中心的实际案例。

希望这个系列的内容，可以完善你的性能测试知识体系，真真正正地帮你解决实际项目中遇到的问题。

那么，作为性能测试系列的第一次分享，我会站在全局的视角，帮你梳理软件性能、软件性能测试相关的知识点，让你对那些你或许已经耳熟能详的性能指标有一个更清晰的理解，为你完成后续的性能测试工作打好基础。

如果你对软件性能的理解还停留在响应时间的快慢上，那么赶紧和我一起开始今天的内容吧。

在开始下面的内容前，请你先思考一个问题：当我们谈及软件性能的时候，我们到底谈的是什么？

目前，对软件性能最普遍的理解就是软件处理的及时性。但其实，从不同的系统类型，以及不同的视角去讨论软件性能，都会有所区别。

对于不同类型的系统，软件性能的关注点各不相同，比如：

Web 类应用和手机端应用，一般以终端用户感受到的端到端的响应时间来描述系统的性能；

非交互式的应用，比如典型的电信和银行后台处理系统，响应时间关注更多的是事件处理的速度，以及单位时间的事件吞吐量。

这很容易理解。同样地，对同一个系统来说，不同的对象群体对软件性能的关注点和期望也不完全相同，甚至很多时候是对立的。这里，不同的对象群体可以分为四大类：终端用户、系统运维人员、软件设计开发人员和性能测试人员。

图 1 衡量软件性能的四个维度

终端用户是软件系统的最终使用者，他们对软件性能的反馈直接决定了这个系统的应用前景；而，软件开发人员、运维人员、性能测试人员，对性能测试的关注点则直接决定了一个系统交付到用户手中的性能。

只有全面了解各类群体对软件系统的不同需求，才能保证这个系统具有真正高可靠的性能。所以，接下来我会从这四类人的视角和维度去分享软件性能到底指的是什么。

终端用户眼中的软件性能

从终端用户（也就是软件系统使用者）的维度来讲，软件性能表现为用户进行业务操作时的主观响应时间。具体来讲就是，从用户在界面上完成一个操作开始，到系统把本次操作的结果以用户能察觉的方式展现出来的全部时间。对终端用户来说，这个时间越短体验越好。

这个响应时间是终端用户对系统性能的最直观印象，包括了系统响应时间和前端展现时间。

系统响应时间，反应的是系统能力，又可以进一步细分为应用系统处理时间、数据库处理时间和网络传输时间等；

前端展现时间，取决于用户端的处理能力。

从这个角度来看，你就可以非常容易理解性能测试为什么会分为后端（服务器端）的性能测试和前端（通常是浏览器端）的性能测试了。

系统运维人员眼中的软件性能

从软件系统运维（也就是系统运维人员）的角度，软件性能除了包括单个用户的响应时间外，更要关注大量用户并发访问时的负载，以及可能的更大负载情况下的系统健康状态、并发处理能力、当前部署的系统容量、可能的系统瓶颈、系统配置层面的调优、数据库的调优，以及长时间运行稳定性和可扩展性。

大多数情况下，系统运维人员和终端用户是站在同一条战线上的，希望系统的响应速度尽可能地快。但，某些情况下他们的意见是对立的，最常见的情况就是，系统运维人员必须在最大并发用户数和系统响应时间之间进行权衡取舍。比如，当有两套系统配置方案可以提供以下系统能力的时：

配置方案 A 可以提供 100 万并发访问用户的能力，此时用户的登录响应时间是 3 秒；

配置方案 B 可以提供 500 万并发访问用户的能力，此时用户的登录响应时间是 8 秒。

这时，从全局利益最大化角度来看，系统具有更大并发用户承载能力的价值会更大，所以运维人员一般都会选择方案 B。

目前，有些系统为了能够承载更多的并发用户，往往会牺牲等待时间而引入预期的等待机制。比如，火车票购票网站，就在处理极大并发用户时采用了排队机制，以尽可能提高系统容量，但却增加了用户实际感受到的响应时间。

软件设计开发人员眼中的软件性能

从软件系统开发（也就是软件设计开发人员）的角度来讲，软件性能关注的是性能相关的设计和实现细节，这几乎涵盖了软件设计和开发的全过程。

在大型传统软件企业中，软件性能绝不仅仅是性能测试阶段要考虑的问题，而是整个软件研发生命周期都要考虑的内容，我们往往把围绕性能相关的活动称为“性能工程”（Performance Engineering）。我曾在惠普软件研发中心的性能测试卓越中心负责这方面的技术工作，所以感受颇深。

在软件设计开发人员眼中，软件性能通常会包含算法设计、架构设计、性能最佳实践、数据库相关、软件性能的可测试性这五大方面。其中，每个方面关注的点，也包括很多。

第一，算法设计包含的点：

核心算法的设计与实现是否高效；

必要时，设计上是否采用 buffer 机制以提高性能，降低 I/O；

是否存在潜在的内存泄露；

是否存在并发环境下的线程安全问题；

是否存在不合理的线程同步方式；

是否存在不合理的资源竞争。

第二，架构设计包含的内容：

站在整体系统的角度，是否可以方便地进行系统容量和性能扩展；

应用集群的可扩展性是否经过测试和验证；

缓存集群的可扩展性是否经过测试和验证；

数据库的可扩展性是否经过测试和验证。

第三，性能最佳实践包含的点：

代码实现是否遵守开发语言的性能最佳实践；

关键代码是否在白盒级别进行性能测试；

是否考虑前端性能的优化；

必要的时候是否采用数据压缩传输；

对于既要压缩又要加密的场景，是否采用先压缩后加密的顺序。

第四，数据库相关的点：

数据库表设计是否高效；

是否引入必要的索引；

SQL 语句的执行计划是否合理；

SQL 语句除了功能是否要考虑性能要求；

数据库是否需要引入读写分离机制；

系统冷启动后，缓存大量不命中的时候，数据库承载的压力是否超负荷。

第五，软件性能的可测试性包含的点：

是否为性能分析（Profiler）提供必要的接口支持；

是否支持高并发场景下的性能打点；

是否支持全链路的性能分析。

需要注意的是，软件开发人员一般不会关注系统部署级别的性能，比如软件运行目标操作系统的调优、应用服务器的参数调优、数据库的参数调优、网络环境的调优等。

系统部署级别的性能测试，目前一般是在系统性能测试阶段或者系统容量规划阶段，由性能测试人员、系统架构师，以及数据库管理员（DBA）协作完成。

性能测试人员眼中的软件性能

从性能工程的角度看，性能测试工程师关注的是算法设计、架构设计、性能最佳实践、数据库相关、软件性能的可测试性这五大方面。

在系统架构师、DBA，以及开发人员的协助下，性能测试人员既要能够准确把握软件的性能需求，又要能够准确定位引起“不好”性能表现的制约因素和根源，并提出相应的解决方案。

一个优秀的性能测试工程师，一般需要具有以下技能：

性能需求的总结和抽象能力；

根据性能测试目标，精准的性能测试场景设计和计算能力；

性能测试场景和性能测试脚本的开发和执行能力；

测试性能报告的分析解读能力；

性能瓶颈的快速排查和定位能力；

性能测试数据的设计和实现能力；

面对互联网产品，全链路压测的设计与执行能力，能够和系统架构师一起处理流量标记、影子数据库等的技术设计能力；

深入理解性能测试工具的内部实现原理，当性能测试工具有限制时，可以进行扩展二次开发；

极其宽广的知识面，既要有“面”的知识，比如系统架构、存储架构、网络架构等全局的知识，还要有大量“点”的知识积累，比如数据库 SQL 语句的执行计划调优、JVM 垃圾回收（GC）机制、多线程常见问题等等。

看到如此多的技能要求你可能有点害怕，的确，性能测试的专业性比较强，我经常把优秀的性能工程师比作是优秀的医生，也是这个原因。你需要在实际项目中积累大量的实际案例，才能慢慢培养所谓的“性能直觉”，从我个人的学习路径来讲也是如此。

天下无难事只怕有心人，所以抓住一切可以充实自己的机会吧，我们终将会破茧成蝶。

这就是终端用户、系统运维工程师、软件开发工程师，以及性能测试工程师眼中的性能测试了，至此我们也就非常容易理解，不同的群体对同一个系统的性能要求为什么会如此不同。

现在，我再来和你说说衡量软件性能的三个最常用的指标：并发用户数、响应时间，以及系统吞吐量。只要你接触过性能测试，或者你的团队开展过性能测试，你都应该听说这三个指标。但其实很多人对它们的理解还都停留在表面，并没有深入细致地考虑过其本质与内涵，这也导致了性能测试很多时候并没有发挥应有的作用。

因此，接下来我会和你深入地聊聊这三个指标的内涵和外延，帮助你获得一个全新的认识。

并发用户数

并发用户数，是性能需求与测试最常用，也是最重要的指标之一。它包含了业务层面和后端服务器层面的两层含义。

业务层面的并发用户数，指的是实际使用系统的用户总数。但是，单靠这个指标并不能反映系统实际承载的压力，我们还要结合用户行为模型才能得到系统实际承载的压力。

后端服务器层面的并发用户数，指的是“同时向服务器发送请求的数量”，直接反映了系统实际承载的压力。

为了让你更好地理解这两层含义之间的区别，我们先一起来看一个实例：一个已经投入运行的 ERP 系统，该系统所在企业共有 5000 名员工并都拥有账号，也就是说这个系统有 5000 个潜在用户。

根据系统日志分析得知，该系统最大在线用户数是 2500 人，那么从宏观角度来看，2500 就是这个系统的最大并发用户数。但是，2500 这个数据仅仅是说在最高峰时段有 2500 个用户登录了系统，而服务器所承受的压力取决于登录用户的行为，所以它并不能准确表现服务器此时此刻正在承受的压力。

假设在某一时间点上，这 2500 个用户中，30% 用户处于页面浏览状态（对服务器没有发起请求），20% 用户在填写订单（也没有对服务器发起请求），5% 用户在递交订单，15% 用户在查询订单，而另外的 30% 用户没有进行任何操作。那么此时，这 2500 个“并发用户”中真正对服务器产生压力的只有 500 个用户（（5%+15%）*2500=500）。

在这个例子中，5000 是最大的“系统潜在用户数”，2500 是最大的“业务并发用户数”，500 则是某个时间点上的“实际并发用户数”。而此时这 500 个用户同时执行业务操作所实际触发的服务器端的所有调用，叫作“服务器并发请求数”。

从这个例子可以看出，在系统运行期间的某个时间点上，有一个指标叫作“同时向服务器发送请求的数量”，这个“同时向服务器发送请求的数量”就是服务器层面的并发用户数，这个指标同时取决于业务并发用户数和用户行为模式，而且用户行为模式占的比重较大。

因此，分析得到准确的用户行为模式，是性能测试中的关键一环。但，获得精准的用户行为模式，是除了获取性能需求外，最困难的工作。

目前，获取用户行为模式的方法，主要分为两种：

对于已经上线的系统来说，往往采用系统日志分析法获取用户行为统计和峰值并发量等重要信息；

而对于未上线的全新系统来说，通常的做法是参考行业中类似系统的统计信息来建模，然后分析。

响应时间

通俗来讲，响应时间反映了完成某个操作所需要的时间，其标准定义是“应用系统从请求发出开始，到客户端接收到最后一个字节数据所消耗的时间”，是用户视角软件性能的主要体现。

响应时间，分为前端展现时间和系统响应时间两部分。其中，前端时间，又称呈现时间，取决于客户端收到服务器返回的数据后渲染页面所消耗的时间；而系统响应时间，又可以进一步划分为 Web 服务器时间、应用服务器时间、数据库时间，以及各服务器间通信的网络时间。

除非是针对前端的性能测试与调优，软件的性能测试一般更关注服务器端。但是，服务器端响应时间的概念非常清晰、直接，就是指从发出请求起到处理完成的时间，没有二义性；而前端时间的定义，在我看来存在些歧义。所以，接下来我会和你详细聊聊前端时间这个话题。

虽然前端时间一定程度上取决于客户端的处理能力，但是前端开发人员现在还会使用一些编程技巧在数据尚未完全接收完成时呈现数据，以减少用户实际感受到的主观响应时间。也就是说，我们现在会普遍采用提前渲染技术，使得用户实际感受到的响应时间通常要小于标准定义的响应时间。

鉴于此，我认为响应时间的标准定义就不尽合理了，尤其是对于“接收到最后一个字节”。

我来举个实际案例吧。加载一个网页时，如果 10 秒后还是白屏，那你一定会感觉很慢、性能无法接受。但是，回想一下你曾经上新浪网的经历，当加载新浪首页时，你应该不会感觉速度很慢吧。其实，实际情况是，新浪首页的加载时间要远大于 10 秒，只是新浪采用了数据尚未完全接收完成时进行呈现的技术，大大缩短了用户主观感受到的时间，提升了用户体验。

所以，严格来讲，响应时间应该包含两层含义：技术层面的标准定义和基于用户主观感受时间的定义。而在性能测试过程中，我们应该使用哪个层面的含义将取决于性能测试的类型。显然，对于软件服务器端的性能测试肯定要采用标准定义，而对于前端性能评估，则应该采用用户主观感受时间的定义。

当然，我们在前端性能测试中，会利用一些事件的触发（比如 DOM-Load、Page-load 等）来客观地衡量“主观的前端性能”。这部分内容我会在后面介绍前端性能测试时，和你详细讨论。

系统吞吐量

系统吞吐量，是最能直接体现软件系统负载承受能力的指标。

这里需要注意的是，所有对吞吐量的讨论都必须以“单位时间”作为基本前提。其实，我认为把“Throughput”翻译成吞吐率更贴切，因为我们可以这样理解：吞吐率 = 吞吐量 / 单位时间。但既然国内很多资料已经翻译为了“吞吐量”，所以通常情况下我们不会刻意去区分吞吐量和吞吐率，统称为吞吐量。

对性能测试而言，通常用“Requests/Second”“Pages/Second”“Bytes/Second”来衡量吞吐量。当然，从业务的角度来讲，吞吐量也可以用单位时间的业务处理数量来衡量。

以不同方式表达的吞吐量可以说明不同层次的问题。比如：

“Bytes/Second”和“Pages/Second”表示的吞吐量，主要受网络设置、服务器架构、应用服务器制约；

“Requests/Second”表示的吞吐量，主要受应用服务器和应用本身实现的制约。

这里需要特别注意的是：虽说吞吐量可以反映服务器承受负载的情况，但在不同并发用户数的场景下，即使系统具有相近的吞吐量，但是得到的系统性能瓶颈也会相差甚远。

比如，某个测试场景中采用 100 个并发用户，每个用户每隔 1 秒发出一个 Request，另外一个测试场景采用 1000 个并发用户，每个用户每隔 10 秒发出一个 Request。显然这两个场景具有相同的吞吐量, 都是 100 Requests/second，但是两种场景下的系统性能拐点肯定不同。因为，两个场景所占用的资源是不同的。

这就要求性能测试场景的指标，必然不是单个，需要根据实际情况组合并发用户数、响应时间这两个指标。

总结

作为性能测试系列的第一篇文章，我和你一起梳理了软件性能、软件性能测试相关的知识点，旨在你对加深软件性能指标的理解，为后续的性能测试实战打好基础。

首先，我从终端用户、系统运维人员、软件设计开发人员和性能测试人员，这四个维度介绍了软件系统的性能到底指的是什么：

终端用户希望自己的业务操作越快越好；

系统运维人员追求系统整体的容量和稳定；

开发人员以“性能工程”的视角关注实现过程的性能；

性能测试人员需要全盘考量、各个击破。

然后，我介绍了软件性能的三个最常用的指标：并发用户数，响应时间，系统吞吐量：

并发用户数包含不同层面的含义，既可以指实际的并发用户数，也可以指服务器端的并发数量；

响应时间也包含两层含义，技术层面的标准定义和基于用户主观感受时间的定义；

系统吞吐量是最能直接体现软件系统承受负载能力的指标，但也必须和其他指标一起使用才能更好地说明问题。

思考题

系统吞吐量的表现形式有很多，比如“Requests/Second”“Pages/Second”“Bytes/Second”等，你在性能测试项目中，选择具体的系统吞吐量指标时，会考虑哪些因素呢？

欢迎你给我留言。

29 | 聊聊性能测试的基本方法与应用领域

你好，我是茹炳晟。今天我和你分享的主题是：聊聊性能测试的基本方法与应用领域。

在上一次分享《带你一起解读不同视角的软件性能与性能指标》这个主题时，我介绍了衡量软件性能的三个最主要的指标：并发用户数、响应时间和系统吞吐量，和你分享了这个指标的内涵和外延。

所以，今天我会先继续上次的话题，和你分享并发用户数、响应时间和系统吞吐量这三个指标之间的关系和约束；然后，我会再和你分享性能测试七种常用方法，以及四大应用领域。

由于性能测试是一个很宽泛的话题，所以不同的人对性能测试的看法也不完全一样，同样一种方法可能也会有不同的表述方式。但是，从我亲身经历的实践来看，我们最关键的还是要去理解这些方法的本质和内涵，这样在面对实际问题时才能处变不惊，灵活应对。

虽然关于概念、方法和原理的内容会有些枯燥，但是掌握了这些看似枯燥的内容后，你会发现自己的性能测试知识体系越发完善了。当然，在这些看似枯燥的理论讲解中，我也会通过类比的方式，帮助你理解。如果你觉得不过瘾，还想知道一些更细节的实现，欢迎你给我留言，我们一起来讨论。

并发用户数、响应时间、系统吞吐量之间的关系

并发用户数、响应时间、系统吞吐量，这三个名词的含义可能就已经让你感觉云里雾里了，因此我会通过一个我们日常生活中的体检为例，再来解释一下它们到底是什么，以及它们之间的关系和约束。

你先来想象这样一个场景：假设你找了一份新工作，入职前需要到体检中心完成入职体检。

在体检中心做检查的过程，通常是先到前台登记个人信息并领取体检单，然后根据体检单的检查项目依次完成不同科室的检查。

假设一共有 5 个科室，每个科室有 3 个候诊室，你发现体检中心有很多人都在做检查，那么你一般会选择先做排队人数较少的检查项目，直至完成 5 个科室的全部检查，最后离开体检中心。

现在，我们做个类比：把整个体检中心想象成一个软件系统，从你进入体检中心到完成全部检查离开所花费的时间就是响应时间，而同时在体检中心参加体检的总人数就是并发用户数，那么系统吞吐量就可以想象成是单位时间内完成体检的人数，比如每小时 100 人。

如果你到达体检中心的时间比较早，这时人还很少，5 个科室都不用排队，那么你就能以最短的时间完成体检。

也就是说，当系统的并发用户数比较少时，响应时间就比较短；但是由于整体的并发用户数少，所以系统的吞吐量也很低。从中，我们可以得出这样的结论：

当系统并发用户数较少时，系统的吞吐量也低，系统处于空闲状态，我们往往把这个阶段称为 “空闲区间”。

如果你到达体检中心时，这里的人已经比较多了，只有部分科室不需要排队，但好在每个科室都有 3 个候诊室同时进行检查，所以排队时间不会很长，你还是可以在较短的时间完成体检。

也就是说，当系统的并发用户数比较多时，响应时间不会增加太多，因此系统的整体吞吐量也随着并发用户数的变大而变大的。从中，我们可以得出这样的结论：

当系统整体负载并不是很大时，随着系统并发用户数的增长，系统的吞吐量也会随之呈线性增长，我们往往把这个阶段称为 “线性增长区间”。

但是，当体检中心的人越来越多时，每个科室都需要排队，而且每个科室的队伍都很长，你每检查完一个项目都要花很长时间去排队进行下一个检查项目。这样一来，你完成体检的时间就会明显变长。

也就是说，系统的并发用户数达到一定规模时，每个用户的响应时间都会明显变长，所以系统的整体吞吐量并不会继续随着并发用户数的增长而增长。从中，我们可以得出这样的结论：

随着系统并发用户数的进一步增长，系统的处理能力逐渐趋于饱和，因此每个用户的响应时间会逐渐变长。相应地，系统的整体吞吐量并不会随着并发用户数的增长而继续呈线性增长。我们往往把这个阶段称为系统的“拐点”。

最糟糕的情况来了，如果体检中心的人继续增加，你会发现连排队、站人的地方都没有了，所有人都被堵在了一起，候诊室中检查完的人出不来，排队的人又进不去。

也就是说，系统的并发用户数已经突破极限，每个用户的响应时间变得无限长，因此系统的整体吞吐量变成了零。换言之，此时的系统已经被压垮了。从中，我们可以得出这样的结论：

随着系统并发用户数的增长，系统处理能力达到过饱和状态。此时，如果继续增加并发用户数，最终所有用户的响应时间会变得无限长。相应地，系统的整体吞吐量会降为零，系统处于被压垮的状态。我们往往把这个阶段称为“过饱和区间”。

通过这个类比，相信你已经对并发用户数、响应时间和系统吞吐量理解得更透彻了，对于它们之间的关系和约束，也都了然于胸了。

只有理解了这些主要性能指标之间的约束关系，我们才能在实际的性能测试实践中设计有的放矢的性能测试场景。比如，后端性能测试的测试负载，我们一般只会把它设计在“线性增长区间”内；而压力测试的测试负载，我们则会将它设计在系统“拐点”上下，甚至是“过饱和区间”。

那么，接下来让我们一起来看一下性能测试的方法都有哪些。

常用的七种性能测试方法

根据在实际项目中的实践经验，我把常用的性能测试方法分为七大类：后端性能测试（Back-end Performance Test）、前端性能测试（Front-end Performance Test）、代码级性能测试（Code-level Performance Test）、压力测试（Load/Stress Test）、配置测试（Configuration Test）、并发测试（Concurrence Test），以及可靠性测试（Reliability Test）。接下来，我将详细为你介绍每一种测试方法。

第一，后端性能测试

其实，你平时听到的性能测试，大多数情况下指的是后端性能测试，也就是服务器端性能测试。

后端性能测试，是通过性能测试工具模拟大量的并发用户请求，然后获取系统性能的各项指标，并且验证各项指标是否符合预期的性能需求的测试手段。

这里的性能指标，除了包括并发用户数、响应时间和系统吞吐量外，还应该包括各类资源的使用率，比如系统级别的 CPU 占用率、内存使用率、磁盘 I/O 和网络 I/O 等，再比如应用级别以及 JVM 级别的各类资源使用率指标等。

由于需要模拟的并发用户数，通常在“几百”到“几百万”的数量级，所以你选择的性能测试工具，一定不是基于 GUI 的，而是要采用基于协议的模拟方式，也就是去模拟用户在 GUI 操作的过程中实际向后端服务发起的请求。

只有这样才能模拟很高的并发用户数，尽可能地模拟出真实的使用场景，这也是现在所有后端性能测试工具所采用的方法。

根据应用领域的不同，后端性能测试的场景设计主要包括以下两种方式：

基于性能需求目标的测试验证；

探索系统的容量，并验证系统容量的可扩展性

第二，前端性能测试

前端性能测试并没有一个严格的定义和标准。

通常来讲，前端性能关注的是浏览器端的页面渲染时间、资源加载顺序、请求数量、前端缓存使用情况、资源压缩等内容，希望借此找到页面加载过程中比较耗时的操作和资源，然后进行有针对性的优化，最终达到优化终端用户在浏览器端使用体验的目的。

目前，业界普遍采用的前端测试方法，是雅虎（Yahoo）前端团队总结的 7 大类 35 条前端优化规则，你可以通过雅虎网站查看这些规则，以及对各规则的详细解读。

我在这里列出了其中几个最典型也是最重要的规则，来帮助你理解前端性能测试优化的关注范围。

减少 http 请求次数：http 请求数量越多，执行过程耗时就越长，所以可以采用合并多个图片到一个图片文件的方法来减少 http 请求次数，也可以采用将多个脚本文件合并成单一文件的方式减少 http 请求次数；

减少 DNS 查询次数：DNS 的作用是将 URL 转化为实际服务器主机 IP 地址，实现原理是分级查找，查找过程需要花费 20~100ms 的时间，所以一方面我们要加快单次查找的时间，另一方面也要减少一个页面中资源使用了多个不同域的情况；

避免页面跳转：页面跳转相当于又打开一个新的页面，耗费的时间就会比较长，所以要尽量避免使用页面跳转；

使用内容分发网络（CDN）：使用 CDN 相当于对静态内容做了缓存，并把缓存内容放在网络供应商（ISP）的机房，用户根据就近原则到 ISP 机房获取这些被缓存了的静态资源，因此可以大幅提高性能；

Gzip 压缩传输文件：压缩可以帮助减小传输文件的大小，进而可以从网络传输时间的层面来减少响应时间；

第三，代码级性能测试

代码级性能测试，是指在单元测试阶段就对代码的时间性能和空间性能进行必要的测试和评估，以防止底层代码的效率问题在项目后期才被发现的尴尬。

如果你从事过性能测试相关的工作，一定遇到过这样的场景：系统级别的性能测试发现一个操作的响应时间很长，然后你要花费很多时间去逐级排查，最后却发现罪魁祸首是代码中某个实现低效的底层算法。这种自上而下的逐级排查定位的方法，效率通常都很低，代价也很高。

所以，我们就需要在项目早期，对一些关键算法进行代码级别的性能测试，以防止此类在代码层面就可以被发现的性能问题，遗留到最后的系统性能测试阶段才被发现。

但是，从实际执行的层面来讲，代码级性能测试并不存在严格意义上的测试工具，通常的做法是：改造现有的单元测试框架。

最常使用的改造方法是：

将原本只会执行一次的单元测试用例连续执行 n 次，这个 n 的取值范围通常是 2000~5000；

统计执行 n 次的平均时间。如果这个平均时间比较长（也就是单次函数调用时间比较长）的话，比如已经达到了秒级，那么通常情况下这个被测函数的实现逻辑一定需要优化。

这里之所以采用执行 n 次的方式，是因为函数执行时间往往是毫秒级的，单次执行的误差会比较大，所以采用多次执行取平均值的做法。

第四，压力测试

压力测试，通常指的是后端压力测试，一般采用后端性能测试的方法，不断对系统施加压力，并验证系统化处于或长期处于临界饱和阶段的稳定性以及性能指标，并试图找到系统处于临界状态时的主要瓶颈点。所以，压力测试往往被用于系统容量规划的测试。

还有些情况，在执行压力测试时，我们还会故意在临界饱和状态的基础上继续施加压力，直至系统完全瘫痪，观察这个期间系统的行为；然后，逐渐减小压力，观察瘫痪的系统是否可以自愈。

第五，配置测试

配置测试，主要用于观察系统在不同配置下的性能表现，通常使用后端性能测试的方法：

通过性能基准测试（Performance Benchmark）建立性能基线（Performance Baseline）；

在此基础上，调整配置；

基于同样的性能基准测试，观察不同配置条件下系统性能的差异，根本目的是要找到特定压力模式下的最佳配置。

这里需要注意的是，“配置”是一个广义配置的概念，包含了以下多个层面的配置：

宿主操作系统的配置；

应用服务器的配置；

数据库的配置；

JVM 的配置；

网络环境的配置；

…

第六，并发测试

并发测试，指的是在同一时间，同时调用后端服务，期间观察被调用服务在并发情况下的行为表现，旨在发现诸如资源竞争、资源死锁之类的问题。

谈到并发测试，我就不得不和你说说“集合点并发”的概念了，它源于 HP 的 LoadRunner，目前已经被广泛使用了。那，到底什么是“集合点并发”呢？

假设我们希望后端调用的并发数是 100，如果直接设定 100 个并发用户是无法达到这个目标的，因为这 100 个并发用户会各自执行各自的操作，你无法控制某一个确定的时间点上后端服务的并发数量。

为了达到准确控制后端服务并发数的目的，我们需要让某些并发用户到达该集合点时，先处于等待状态，直到参与该集合的全部并发用户都到达时，再一起向后端服务发起请求。简单地说，就是先到的并发用户要等着，等所有并发用户都到了以后，再集中向后端服务发起请求。

比如，当要求的集合点并发数是 100 时，那么前 99 个到达的用户都会等在那里，直到第 100 个用户到了，才集中向后端服务发起请求。当然，实际达到服务器的并发请求数，还会因为网络延迟等原因小于 100。

所以，在实际项目中，我建议在要求的并发数上进行适当放大，比如要求的并发数是 100，那我们集合点并发数可以设置为 120。

第七，可靠性测试

可靠性测试，是验证系统在常规负载模式下长期运行的稳定性。

虽然可靠性测试在不同公司的叫法不同，但其本质就是通过长时间模拟真实的系统负载来发现系统潜在的内存泄漏、链接池回收等问题。

由于真实环境下的实际负载，会有高峰和低谷的交替变化（比如，对于企业级应用，白天通常是高峰时段，而晚上则是低峰时段），所以为了尽可能地模拟出真实的负载情况，我们会每 12 小时模拟一个高峰负载，两个高峰负载中间会模拟一个低峰负载，依次循环 3-7 天，形成一个类似于“波浪形”的系统测试负载曲线。

然后，用这个“波浪形”的测试负载模拟真实的系统负载，完成可靠性测试。同样地，可靠性测试也会持续 3-7 天。

聊完了常用性能测试方法的种类后，我们再来简单看一下性能测试的四大应用领域，以及每个应用领域都会使用哪些性能测试方法。

性能测试的四大应用领域

不同的性能测试方法适用于不同的应用领域去解决不同的问题，这里“不同的应用领域”主要包括能力验证、能力规划、性能调优、缺陷发现这四大方面。每个应用领域可以根据自身特点，选择合适的测试方法。

第一，能力验证

能力验证是最常用，也是最容易理解的性能测试的应用领域，主要是验证“某系统能否在 A 条件下具有 B 能力”，通常要求在明确的软硬件环境下，根据明确的系统性能需求设计测试方案和用例。

能力验证这个领域最常使用的测试方法，包括后端性能测试、压力测试和可靠性测试。

第二，能力规划

能力规划关注的是，如何才能使系统达到要求的性能和容量。通常情况下，我们会采用探索性测试的方式来了解系统的能力。

能力规划解决的问题，主要包括以下几个方面：

能否支持未来一段时间内的用户增长；

应该如何调整系统配置，使系统能够满足不断增长的用户数需求；

应用集群的可扩展性验证，以及寻找集群扩展的瓶颈点；

数据库集群的可扩展性验证；

缓存集群的可扩展性验证；

…

能力规划最常使用的测试方法，主要有后端性能测试、压力测试、配置测试和可靠性测试。

第三，性能调优

性能调优，其实是性能测试的延伸。在一些大型软件公司，会有专门的性能工程（Performance Engineering）团队，除了负责性能测试的工作外，还会负责性能调优。

性能调优主要解决性能测试过程中发现的性能瓶颈的问题，通常会涉及多个层面的调整，包括硬件设备选型、操作系统配置、应用系统配置、数据库配置和应用代码实现的优化等等。

这个领域最常用的测试方法，涵盖了我在上面分享的七大类测试方法，即后端性能测试、前端性能测试、代码级性能测试、压力测试、配置测试、并发测试和可靠性测试。

第四，缺陷发现

缺陷发现，是一个比较直接的应用领域，通过性能测试的各种方法来发现诸如内存泄露、资源竞争、不合理的线程锁和死锁等问题。

缺陷发现，最常用的测试方法主要有并发测试、压力测试、后端性能测试和代码级性能测试。

上面这些内容就是性能测试的常用方法和应用领域了，我用一张表汇总了各个应用领域需要用到的测试方法，希望可以帮助你记忆、理解。

| 能力验证 | 能力规划 | 性能调优 | 缺陷发现
后端性能测试 | √ | √ | √ | √
前端性能测试 |  |  | √ |
代码级性能测试 |  |  | √ | √
压力测试 | √ | √ | √ | √
配置测试 |  | √ | √ |
并发测试 |  |  | √ | √
可靠性测试 | √ | √ | √ |

总结

今天我通过一个生活中“体检”的实例，和你分享了并发用户数、响应时间和系统吞吐量三者之间的关系：

当系统整体负载并不是很大时，随着并发用户数的增长，系统的吞吐量也会随之线性增长；

随着并发用户数的进一步增长，系统处理能力逐渐趋于饱和，因此每个用户的响应时间会逐渐变长，相应地，系统的整体吞吐量并不会随着并发用户数的增长而继续线性增长。

如果并发用户数再继续增长，系统处理能力达到过饱和状态，此时所有用户的响应时间会变得无限长，相应地，系统的整体吞吐量会降为零，系统处于被压垮的状态。

然后，我跟你分享了后端性能测试、前端性能测试、代码级性能测试、压力测试、配置测试、并发测试，以及可靠性测试这七种常用的性能测试方法，并探讨了这七种方法在能力验证、能力规划、性能调优和缺陷发现这四种场景下的使用情况。

希望我今天的分享，可以帮你打好性能测试知识体系的基础，解决你在实际项目中遇到的问题。

思考题

我今天提到的性能测试的七种测试方法，以及四大应用领域，你在实际的工程项目中接触过哪些呢？在使用过程中，你有没有遇到过什么问题，又是如何解决的？

感谢你的收听，欢迎给我留言。

30 | 工欲善其事必先利其器：后端性能测试工具原理与行业常用工具简介

你好，我是茹炳晟。今天我和你分享的主题是：工欲善其事必先利其器之后端性能测试工具原理与行业常用工具简介。

我在《聊聊性能测试的基本方法与应用领域》这个主题里介绍了七种测试方法，但不管是什么类型的性能测试方法，都需要去模拟大量并发用户的同时执行，所以性能测试基本都是靠工具实现。没有工具，性能测试将寸步难行。

所以，我今天就从后端性能测试的工具讲起，和你一起讨论它们的实现原理，以及如何用于后端的性能测试。另外，我还会和你分享一些中大型互联网企业选择的性能测试工具。

由于我今天要分享的知识点比较多，而且是相对独立的，所以我会采用问答的形式展开这些内容。我希望通过今天的分享，你能够对以下的问题和知识点有完整、清晰的理解与认识：

后端性能测试和后端性能测试工具之间的关系是什么？

后端性能测试工具和 GUI 自动化测试工具最大的区别是什么？

后端性能测试工具的原理是什么？

后端性能测试中，性能测试场景设计是什么意思，具体会涉及哪些内容？

业内主流的后端性能测试工具有哪些？

后端性能测试和后端性能测试工具之间的关系是什么？

后端性能测试工具是实现后端性能测试的技术手段，但是千万不要简单地把使用后端性能测试工具等同于后端性能测试，它只是后端性能测试中的一个必要步骤而已。

完整的后端性能测试应该包括性能需求获取、性能场景设计、性能测试脚本开发、性能场景实现、性能测试执行、性能结果报告分析、性能优化和再验证。

在这其中，后端性能测试工具主要在性能测试脚本开发、性能场景实现、性能测试执行这三个步骤中发挥作用，而其他环节都要依靠性能测试工程师的专业知识完成。

是不是感觉有点抽象，难以理解呢？我来做个类比吧。

假如你现在要去医院看病，医生会根据你对身体不适的描述，要求你先去验血，并确定需要检查的血液指标。验血是通过专业的医疗仪器分析你的血样，并得到验血报告。

医生拿到验血报告后，根据常年积累的专业知识，然后结合验血报告的各项指标以及指标之间的相互关系判断你的病情，并给出诊断结果以及相应的治疗措施。

同样的验血报告，如果给不懂医术的人看，就是一堆没有意义的数据；如果给一个初级医生看，他可能只能基于单个指标的高低给出可能的推测；但是，如果是给一个具有丰富临床经验的医生看，他往往可以根据这些指标以及它们之间的相互关系给出很明确的诊断结果。

现在，我把这个过程和性能测试做个类比，把性能测试对应到整个看病的过程：

需求获取对应的是你向医生描述身体不适细节的过程，医生需要知道要帮你解决什么问题；

设计性能场景对应的是医生决定需要检查哪些血液指标的过程；

使用性能测试工具对应的是使用医疗仪器分析血样的过程 ;

性能测试报告对应的就是验血报告 ;

性能测试人员分析性能结果报告的过程，对应的是医生解读验血报告的过程；

性能测试人员根据性能报告进行性能优化的过程，对应的是医生根据验血报告判断你的病情，并给出相应治疗措施的过程。

所以，在我看来使用性能测试工具获得性能测试报告只是性能测试过程中的一个必要步骤而已，而得出报告的目的是让性能测试工程师去做进一步的分析，以得出最终结论，并给出性能优化的措施。

后端性能测试工具和 GUI 自动化测试工具最大的区别是什么？

虽然后端性能测试工具和 GUI 自动化测试工具都是通过自动化的手段模拟终端用户使用系统的行为，但是两者实现的原理截然不同。

第一个显著区别是，模拟用户行为的方式。

GUI 自动化测试工具模拟的是用户的界面操作，因此测试脚本记录的是用户在界面上对控件的操作；而性能测试工具模拟的是用户的客户端与服务器之间的通信协议和数据，这些通信协议和数据往往是用户在界面上执行 GUI 操作时产生的。

明白了这一点，你自然就能明白为什么录制虚拟用户性能测试脚本时，我们需要先选定录制协议了。

另外，正是由于脚本的模拟是基于协议的，所以我们才能比较方便地模拟成千上万并发用户同时使用系统的场景；否则，如果性能测试基于 GUI 发起，那我们就需要成千上万的浏览器同时执行用例，而这显然是不可能的。

第二个显著的区别是，测试的执行方式。

GUI 自动化测试的执行，一般是单用户执行并验证功能结果；而性能测试的执行，往往需要同时模拟大量的并发用户，不仅需要验证业务功能是否成功完成，还要收集各种性能监控指标，会涉及到压力产生器、并发用户调度控制、实时监控收集等内容，所以性能测试的执行控制要比 GUI 自动化测试复杂得多。

这部分内容，我稍后在第 32 和 33 这两篇文章中详细展开。

后端性能测试工具的原理是什么？

虽然后端性能测试工具种类很多，但是由于都不能通过 GUI 的方式来模拟并发，所以其基本原理和主要概念基本一致。

首先，后端性能测试工具会基于客户端与服务器端的通信协议，构建模拟业务操作的虚拟用户脚本。对于目前主流的 Web 应用，通常是基于 HTTP/HTTPS 协议；对于 Web Service 应用，是基于 Web Service 协议；至于具体基于哪种协议，你需要和开发人员或者架构师确认，当然现在有些后端性能测试工具也可以直接帮你检测协议的种类。

我们把这些基于协议模拟用户行为的脚本称为虚拟用户脚本，而把开发和产生这些脚本的工具称为虚拟用户脚本生成器。

不同后端性能测试工具的虚拟用户脚本生成器，在使用上的区别比较大：比如，LoadRunner 是通过录制后再修改的方式生成虚拟用户脚本；而 JMeter 主要是通过添加各种组件，然后对组件进行配置的方式生成虚拟用户脚本。

虽然 LoadRunner 也支持采用直接开发的方式产生虚拟用户脚本，但是因为开发难度太大，所以基本上都是采用先录制再开发的方式，不会直接去开发。另外，虽然 JMeter 也支持录制，但是 JMeter 的录制功能是通过设置代理完成的，而且录制出来的脚本都是原始的 http 请求，并没有经过适当的封装，所以录制功能比较弱。

虽然不同工具的使用方式各有特色，但其本质上都是通过协议模拟用户的行为。

然后，开发完成了虚拟用户脚本之后，后端性能测试工具会以多线程或多进程的方式并发执行虚拟用户脚本，来模拟大量并发用户的同时访问，从而对服务器施加测试负载。

其中，我们把实际发起测试负载的机器称为压力产生器。受限于 CPU、内存，以及网络带宽等硬件资源，一台压力产生器能够承载的虚拟用户数量是有限的，当需要发起的并发用户数量超过了单台压力产生器能够提供的极限时，就需要引入多台压力产生器合作发起需要的测试负载。

一旦有了多台压力产生器，那就需要一个专门的控制器来统一管理与协调这些压力产生器，我们把这个专门的控制器称为压力控制器。压力控制器会根据性能测试场景的设计，来控制和协调多台压力产生器上的多线程或多进程执行的虚拟用户脚本，最终模拟出性能测试场景中的测试负载。

接着，在施加测试负载的整个过程中，后端性能测试工具除了需要监控和收集被测系统的各种性能数据以外，还需要监控被测系统各个服务器的各种软硬件资源。比如，后端性能测试工具需要监控应用服务器、数据库服务器、消息队列服务器、缓存服务器等各种资源的占用率。我们通常把完成监控和数据收集的模块称为系统监控器。

在性能测试执行过程中，系统监控器的数据显示界面是性能测试工程师最密切关注的部分，性能测试工程师会根据实时的数据显示来判断测试负载情况下的系统健康状况。

不同的后端测试工具中，系统监控器能力差别也比较大。比如，LoadRunner 的系统监控器就很强大，支持收集各种操作系统的系统参数，还支持与 SiteScope 等第三方专业监控工具的无缝集成。

最后，测试执行完成后，后端性能测试工具会将系统监控器收集的所有信息汇总为完整测试报告，后端性能测试工具通常能够基于该报告生成各类指标的各种图表，还能将多个指标关联在一起进行综合分析来找出各个指标之间的关联性。我们把完成这部分工作的模块称为测试结果分析器。

需要强调的是，测试结果分析器只是按需提供多种不同维度和表现形式的数据展现工作，而对数据的分析工作，还是要依赖于具有丰富经验的性能测试工程师。

后端性能测试场景设计是什么意思，具体会涉及哪些内容？

性能测试场景设计，是后端性能测试中的重要概念，也是压力控制器发起测试负载的依据。

性能测试场景设计，目的是要描述性能测试过程中所有与测试负载以及监控相关的内容。通常来讲，性能测试场景设计主要会涉及以下部分：

并发用户数是多少？

测试刚开始时，以什么样的速率来添加并发用户？比如，每秒增加 5 个并发用户。

达到最大并发用户数后持续多长时间？

测试结束时，以什么样的速率来减少并发用户？比如，每秒减少 5 个并发用户。

需要包含哪些业务操作，各个业务操作的占比是多少？比如，10% 的用户在做登录操作，70% 的用户在做查询操作，其他 20% 的用户在做订单操作。

一轮虚拟用户脚本执行结束后，需要等待多长时间开始下一次执行？

同一虚拟用户脚本中，各个操作之间的等待时间是多少？

需要监控哪些被测服务器的哪些指标？

脚本出错时的处理方式是什么？比如，错误率达到 10% 时，自动停止该脚本。

需要使用多少台压力产生器？

以上这些场景组合在一起，就构成了性能测试场景设计的主要内容。也就是说，性能测试场景会对测试负载组成、负载策略、资源监控范围定义、终止方式，以及负载产生规划作出定义，而其中的每一项还会包含更多的内容。具体请参见如图 1 所示的思维导图。

图 1 性能测试场景的设计

业内主流的后端性能测试工具有哪些？

目前，业内有很多成熟的后端性能测试工具，比如传统的 LoadRunner、JMeter、NeoLoad 等。另外，现在还有很多云端部署的后端性能测试工具或平台，比如 CloudTest、Loadstorm、阿里的 PTS 等。

其中，最为常用的商业工具是 HP 软件（现在已经被 Micro Focus 收购）的 LoadRunner，由于其强大的功能和广泛的协议支持，几乎已经成了性能测试工具的代名词。大量的传统软件企业，也基本都使用 LoadRunner 实施性能测试，所以我在后面分享企业级服务器端性能测试的实践时，也是以 LoadRunner 为基础展开的。

另外，JMeter 是目前开源领域最主流的性能测试工具。JMeter 的功能非常灵活，能够支持 HTTP、FTP、数据库的性能测试，也能够充当 HTTP 代理来录制浏览器的 HTTP 请求，还可以根据 Apache 等 Web 服务器的日志文件回放 HTTP 流量，还可以通过扩展支持海量的并发。

然后，再加上 JMeter 开源免费的特点，已经被很多互联网企业广泛应用。比如，饿了么就是使用 JMeter 来完成系统的全链路压力测试。

其实，传统软件企业偏向于使用 LoadRunner，而互联网企业普遍采用 JMeter，是有原因的。

LoadRunner License 是按照并发用户数收费的，并发用户数越高收费也越贵，但是 LoadRunner 的脚本开发功能、执行控制、系统监控以及报告功能都非常强大，易学易用。

而传统软件企业，需要测试的并发用户数并不会太高，通常是在几百到十几万这个数量级，而且它们很在意软件的易用性和官方支持能力，所以往往热衷于直接选择成熟的商业工具 LoadRunner。

但是，互联网企业的并发用户请求数量很高，很多软件都会达到百万，甚至是千万的级别。那么，如果使用 LoadRunner 的话：

费用会高的离谱；

LoadRunner 对海量并发的测试支持并不太好；

很多互联网企业还会有特定的工具需求，这些特定的需求很难在 LoadRunner 中实现，而在开源的 JMeter 中，用户完全可以根据需求进行扩展。

所以互联网企业往往选用 JMeter 方案，而且通常会自己维护扩展版本。

总结

今天，我以问答的形式，和你分享了后端性能测试的理论，以及工具使用的问题。

首先，我和你解释了后端性能测试和后端性能测试工具之间的关系。在我看来使用性能测试工具获得性能测试报告，只是性能测试过程中的一个必要步骤而已，而得出报告的目的是让性能测试工程师去分析并给出性能优化的措施。

然后，我解释了后端性能测试工具和 GUI 自动化测试工具最大的区别，即它们模拟用户行为的方式以及测试的执行方式不同。

接着，我介绍了后端性能测试工具的基本原理。它首先通过虚拟用户脚本生成器生成虚拟用户脚本；然后根据性能测试场景设计的要求，通过压力控制器控制协调各个压力产生器以并发的方式执行虚拟用户脚本；同时，在测试执行过程中，通过系统监控器收集各种性能指标以及系统资源占用率；最后，通过测试结果分析器展示测试结果数据。

最后，我介绍了性能测试场景设计，并分析了业内主流的后端性能测试工具 LoadRunner 和 JMeter，以及传统软件企业和互联网企业在选择后端性能测试工具时的考量。

思考题

除了我今天提到的后端性能测试工具外，你还接触过哪些后端性能测试工具？这些后端性能测试工具中又有哪些好的设计呢？

感谢你的收听，欢迎你给我留言。

31 | 工欲善其事必先利其器：前端性能测试工具原理与行业常用工具简介

你好，我是茹炳晟。今天我和你分享的主题是：工欲善其事必先利其器之前端性能测试工具原理与行业常用工具简介”。

不同于后端性能测试知识的琐碎、独立，今天我将从问答形式回到正常的分享思路上，为你介绍前端性能测试工具为。我会以一个具体网站为例，和你分析 WebPagetest 的用法，以及前端性能相关的主要概念与指标。

WebPagetest 功能简介

WebPagetest，是前端性能测试的利器：

可以为我们提供全方位的量化指标，包括页面的加载时间、首字节时间、渲染开始时间、最早页面可交互时间、页面中各种资源的字节数、后端请求数量等一系列数据；

还可以自动给出被测页面性能优化水平的评价指标，告诉我们哪些部分的性能已经做过优化处理了，哪些部分还需要改进；

同时，还能提供 Filmstrip 视图、Waterfall 视图、Connection 视图、Request 详情视图和页面加载视频慢动作。

可以说，WebPagetest 为我们提供了前端页面性能测试所需要的一切，而且还是免费的。接下来，我们就通过测试一个具体的网站，实践一下它的强大功能，以及具体使用方法吧。

使用 WebPagetest 测试某网站的首页

那么，接下来我就以某网站首页的前端性能测试和评估为例，和你一起看看如何使用这个强大的前端性能工具。

首先，访问 WebPagetest 的主页“http://www.webpagetest.org/” ，也就是工具的使用界面。

将被测页面的地址填写到被测 Website URL 栏中；

选择测试发起的地域（Test Location）以及你希望使用的浏览器，这里我选择了美国旧金山、Chrome 浏览器。

WebPagetest 在全球各大洲的很多国家和地区都有自己的测试代理机，这些测试代理机可能是虚拟机，也可能是真机，还有很多是建立在 Amazon EC2 上的云端机器。另外，WebPagetest 除了支持各种浏览器以外，还支持主流的 Android 设备和 Apple 设备。

然后，选择需要模拟的网络情况。这里我选择了有线网络 Cable，当然你也可以根据你的测试要求选择各种 3G 或者 4G 移动网络。

接着，在 Repeat View 中选择“First View and Repeat View”。这是一个很关键的选项。我们知道当使用浏览器访问网站时，第一次打开一个新的网页往往会很慢，而第二次打开通常就会快很多，这是因为浏览器端会缓存很多资源。这个选项的意思就是既要测试第一次打开页面的前端性能（First View），也要测试在有缓存情况下重复打开的前端性能（Repeat View）。

最后，点击“Start Test”发起测试。最终的测试设置界面，如图 1 所示。由于全球所有的用户会共享这些散布在各地的测试代理机，所以发起测试后，一般情况下我们的测试并不会被立即执行，而是会进入排队系统。当然，WebPagetest 界面会清楚地告诉你排在第几位。

图 1 WebPagetest 的测试执行界面

测试执行完成后，我们会看到如图 2 所示的测试结果页面。这个页面包含非常多的信息，接下来我会一一解读这些信息，同时跟你分享前端性能指标相关的概念。

图 2 WebPagetest 的测试结果页面

前端性能评估结果评分

图 2 右上角的性能评估结果栏，列出了主要性能评估项目的评分。可以看到“First Byte Time”“Keep-alive Enabled”和“Compress Transfer”三项的评分都是 A 级，说明这三项做得比较好。但是，“Compress Images”“Cache static content”和“Effective use of CDN”的评分比较差，是需要优化的部分。

那么，接下来我们就看看这六项前端性能指标分别代表什么涵义。

第一，First Byte Time

First Byte Time，指的是用户发起页面请求到接收到服务器返回的第一个字节所花费的时间。这个指标反映了后端服务器处理请求、构建页面，并且通过网络返回所花费的时间。

本次测试的结果，首次打开页面（First View）花费的时间是 999 ms，重复打开页面（Repeat View）花费的时间是 860 ms。这两个指标都在 1 s 以下，所以 WebPagetest 给出了 A 级的评分。

第二，Keep-alive Enabled

页面上的各种资源（比如，图片、JavaScript、CSS 等）都需要通过链接 Web 服务器来一一获取，与服务器建立新链接的过程往往比较耗费时间，所以理想的做法是尽可能重用已经建立好的链接，而避免每次使用都去创建新的链接。

Keep-alive Enabled 就是，要求每次请求使用已经建立好的链接。它属于服务器上的配置，不需要对页面本身进行任何更改，启用了 Keep-alive 通常可以将加载页面的时间减少 40%~50％，页面的请求数越多，能够节省的时间就越多。

如图 3 所示，本次测试的结果显示，所有的请求都复用了同一个链接，所以 WebPagetest 也给出了 A 级的评分。

图 3 Keep-alive Enabled 的统计结果

第三，Compress Transfer

如果将页面上的各种文本类的资源，比如 Html、JavaScript、CSS 等，进行压缩传输，将会减少网络传输的数据量，同时由于 JavaScript 和 CSS 都是页面上最先被加载的部分，所以减小这部分的数据量会加快页面的加载速度，同时也能缩短 First Byte Time。

为文本资源启用压缩通常也是服务器配置更改，无需对页面本身进行任何更改。

如图 4 所示，本次测试结果显示，这个网站绝大多数的文本类资源都通过 GZip 进行了压缩，所以 WebPagetest 也给出了 A 级的评分。但是第 13 和第 20 项的两个资源并没有被压缩，报告中显示如果这两个资源也经过压缩，将可以减少额外的 24.3KB 的数据传输量。

图 4 Compress Transfer 的统计结果

第四，Compress Images

为了减少需要网络传输的数据量，图像文件也需要进行压缩处理。显然本次测试结果显示（图 5），所有的 JPEG 格式图片都没有经过必要的压缩处理，并且所有的 JPEG 格式图片都没有使用渐进式 JPEG（Progressive JPEG）技术，所以 WebPagetest 给出了 D 级的评分。

如果你不是专门做前端的工程师，可能并不知道什么是渐进式 JPEG，没关系，我来简单解释一下吧。

普通 JPEG 文件存储方式是按从上到下的扫描方式，把每一行顺序地保存在 JPEG 文件中。打开这个文件显示它的内容时，数据将按照存储时的顺序从上到下一行一行地被显示，直到所有的数据都被读完，就完成了整张图片的显示。

如果文件较大或者网络下载速度较慢，就会看到图片是被一行一行加载的。为了更好的用户体验，渐进式 JPEG 技术就出现了。

渐进式 JPEG 包含多次扫描，然后将扫描顺序存储在 JPEG 文件中。打开文件的过程，会先显示整个图片的模糊轮廓，随着扫描次数的增加，图片会变得越来越清晰。这种格式的主要优点是在网络较慢时，通过图片轮廓就可以知道正在加载的图片大概是什么。

所以，这种技术往往被一些网站用于打开较大图片。

图 5 Compress Images 的统计结果

第五，Cache Static Content

一般情况下，页面上的静态资源不会经常变化，所以如果你的浏览器可以缓存这些资源，那么当重复访问这些页面时，就可以从缓存中直接使用已有的副本，而不需要每次向 Web 服务器请求资源。这种做法，可以显著提高重复访问页面的性能，并减少 Web 服务器的负载。

如图 6 所示，本次测试结果显示，被测网站有超过一半的静态资源没有被浏览器缓存，每次都需要从 Web 服务器端获取，所以 WebPagetest 给出了 F 级的评分。

图 6 Cache Static Content 的统计结果

第六，Effective use of CDN

首先，我来解释一下什么是 CDN。

CDN 是内容分发网络的缩写，其基本原理是采用各种缓存服务器，将这些缓存服务器分布到用户访问相对集中的地区的网络供应商机房内，当用户访问网站时，利用全局负载技术将用户的访问指向距离最近的、工作正常的缓存服务器上，由缓存服务器直接响应用户请求。

理解了什么是 CDN 后，我们再一起来看一下本次测试中 CDN 的使用情况。如图 7 所示，显然本次被测网站绝大多数的资源并没使用 CDN。也许是由于本次发起测试的机器是在美国旧金山，而旧金山可能并不是该网站的目标市场，所以它并没有在这里的 CDN 上部署资源。

图 7 CDN 使用的统计结果

其他前端性能指标解读

图 8 WebPagetest 测试显示的前端性能指标

现在，我们再回过头来看看如图 8 所示的表格，这个表格包含了很多的前端性能指标，大多数指标，我们可以从字面上很容易理解其含义，比如 Load Time、First Byte、Requests 等，我就不再赘述了。

但是，Start Render、First Interactive 和 Speed Index 这三个指标，相对较难理解，所以我会简单为你解释一下。

第一，Start Render

Start Render，指的是浏览器开始渲染的时间，从用户角度看就是在页面上看到第一个内容的时间。该时间决定了用户对页面加载快慢的的第一直观印象，这个时间越短用户会感觉页面速度越快，这样用户也会有耐心去等待其他内容的展现。如果这个时间过长，则用户会在长时间内面对一个空白页面后，失去耐心。

理论上讲，Start Render 时间主要由三部分组成，分别是“发起请求到服务器返回第一个字节的时间（也就是 First Byte 时间）”“从服务器加载 HTML 文档的时间”，以及“HTML 文档头部解析完成所需要的时间”，因此影响 Start Render 时间的因素就包括服务器响应时间、网络传输时间、HTML 文档的大小以及 HTML 头中的资源使用情况。

本次测试中，第一次打开网页的 Start Render 时间是 5 s，而第二次打开网页的 Start Render 时间是 1.83 s。理想的 Start Render 时间并没有严格的标准，一般来情况下，这个值最好不要大于 3 s，所以这个网站还可以再优化一下这个指标。

优化的基本思路是先找出时间到底花在了哪里，由于第二次的结果还是比较理想，所以可以从首次资源加载的角度找出突破口。

第二，First Interactive

First Interactive，可以简单地理解为最早的页面可交互时间。页面中可交互的内容，包括很多种类，比如点击一个链接、点击一个按钮都属于页面可交互的范畴。First Interactive 时间的长短对用户体验的影响十分重要，决定着用户对页面功能的使用，这个值越短越好。

为了使这个值尽可能得小，我们通常会采取以下措施：

只有页面控件内容渲染出来了，才有可能进行交互，所以 First Interactive 依赖于 Start Render 时间。

尽量将交互控件的代码放置在 HTML BODY 的前部，让其尽早加载。

尽早做 JavaScript 的初始化和绑定，目前大多数做法有两种，一是在 DOM Ready 中做所有 JavaScript 的初始化和功能绑定，二是在页面底部做 JavaScript 的初始化和功能绑定。

这两种方式的优点在于简单，不需要关注具体 DOM 结点的位置；缺点则在于初始化的时间太晚。因此，应该将 JavaScript 的初始化提前到相关 DOM 元素建立起来后进行，例如将初始化的操作直接放在元素之后进行，这样就可以使控件尽可能早地变成可交互状态。

本次测试中，第一次打开网页的 First Interactive 时间是 7.788 s，而第二次打开网页的 First Interactive 时间是略大于 1.686 s 的某个值。理想的 First Interactive 时间也没有严格的标准，一般情况下，这个值最好不要大于 5 s，所以这个网站还可以根据上面的三条措施再优化一下这个指标。

第三，Speed Index

严格来说，Speed Index 是通过微积分定义的。我们理解起来会比较困难，所以在这里我们和你只做定性的讨论。

通常，影响网页性能体验的一个重要指标是页面打开时间。打开时间越短，其体验越好。但是，当存在两个打开时间完全相同的网站 A 和 B 时，其中网站 A 的打开过程是逐渐渲染页面完成的，而网站 B 的打开过程则是空白了一段时间后在短时间内完成页面渲染完成的。

毫无疑问，网站 A 的用户体验一定好于 B。Speed Index 就是用来衡量这种体验的，通常来讲，它的值越小越好。

本次测试中，第一次打开网页的 Speed Index 是 8036，而第二次打开网页的 Speed Index 是 2373。

WebPagetest 实际使用中需要解决的问题

讨论到这里，你是不是觉得 WebPagetest 是一个很强大的免费工具，但是如果想要在实际工程项目中全面推行该工具的话，还需要解决两个问题。

第一个问题是，如果被测网站部署在公司内部的网络中，那么处于外网的 WebPagetest 就无法访问这个网站，也就无法完成测试。要解决这个问题，你需要在公司内网中搭建自己的私有 WebPagetest 以及相关的测试发起机。具体如何搭建，你可以参考WebPagetest 官网的建议，这里我就不再继续展开了。

第二个问题是，用 WebPagetest 执行前端测试时，所有的操作都是基于界面操作的，不利于与 CI/CD 的流水线集成。要解决这个问题，就必须引入 WebPagetest API Wrapper。

WebPagetest API Wrapper 是一款基于 Node.js，调用了 WebPagetest 提供的 API 的命令行工具。也就是说，你可以利用这个命令行工具发起基于 WebPagetest 的前端性能测试，这样就可以很方便地与 CI/CD 流水线集成了。具体的使用步骤如下：

通过“npm install webpagetest -g”安装该命令行工具；

访问 https://www.webpagetest.org/getkey.php 获取你的 WebPagetest API Key；

使用“webpagetest test -k API-KEY 被测页面 URL”发起测试，该调用是异步操作，会立即返回，并为你提供一个 testId；

使用“webpagetest status testId”查询测试是否完成；

测试完成后，就可以通过“webpagetest results testId”查看测试报告，但是你会发现测试报告是个很大的 JSON 文件，可读性较差；

通过“npm install webpagetest-mapper -g”安装 webpagetest-mapper 工具，这是为了解决测试报告可读性差的问题，将 WebPagetest 生成的 JSON 文件格式的测试报告转换成为 HTML 文件格式；

使用“Wptmap -key API-KEY --resultIds testId --output ./test.html”将 JSON 文件格式的测试结果转换成 HTML 格式。

总结

今天，我使用 WebPagetest 测试了一个具体网站的前端性能。在解读这个测试报告的同时，我和你分享了几个重要的前端性能指标，以及如何提升这些指标，最终达到优化网站用户体验的目的。

虽然，WebPagetest 是一款免费的、功能强大的前端性能测试工具，但是用于实际测试项目时，往往还存在两个方面的问题，我给你分析了这两个问题出现的原因，以及如何解决这两个问题，以达到更高效地完成前端性能测试的目的。

思考题

除了我今天介绍的 WebPagetest 外，前端测试工具还有 YSlow 等。你还在工作中接触过哪些前端性能测试工具，它们各自有什么特点呢？

感谢收听，欢迎你给我留言。

32-无实例无真相：基于LoadRunner实现企业级服务器端性能测试的实践（上）

你好，我是茹炳晟。今天我和你分享的主题是：无实例无真相之基于 LoadRunner 实现企业级服务器端性能测试的实践（上）。

从今天开始的两篇文章，我将介绍如何基于 LoadRunner 实际开展企业级服务器端的性能测试。分享这个主题时，我会从最开始的性能需求获取开始讲起，带你完整地经历一个实际服务器端性能测试项目。通过这个过程，我希望可以帮助你快速建立服务器端性能测试的全局观，并了解各主要步骤的关键技术细节。

听到这里，你可能就有些困惑了。我在分享《工欲善其事必先利其器：后端性能测试工具原理与行业常用工具简介》这个主题时，曾经说到：LoadRunner 比较适合于传统软件企业开展性能测试，而 JMeter 更适用于互联网企业的软件性能测试。那么，为什么我没有选择以 JMeter 为例来展开后端性能测试呢？

我选择 LoadRunner，是经过深思熟虑的，主要原因包括：

JMeter 的官方文档对其使用方法介绍得很详细，而且其操作基本属于“傻瓜式”的。JMeter 使用的难点在于：如何支持海量并发，以及实现更好的 load 控制，解决这个问题你可以参考 LoadRunner 的实现方式，然后从你所在企业的实际业务场景出发，进行二次开发。

互联网企业和传统软件企业的软件产品的后端性能测试，在原理以及基本方法上是基本一致的，区别较大的只是全链路压测。所以，我以传统企业的软件产品为例展开分享，你因此学到的原理以及测试方法将同样适用于互联网软件产品的性能测试。

关于互联网软件产品的全链路压测，由于需要实现海量并发以及流量隔离等操作，所以目前只有一些大型企业在做，比如饿了么、淘宝、ebay、美团等超级大的网站。但是，如果你也想了解全链路压测的话，我也会准备一篇“加餐”文章，和你分享开展全链路压测的难点，以及应对方案。我会更新完性能测试这个系列以后，为你准备这篇“加餐”文章。

为了让你在进行服务器端性能测试时更充分地利用好 LoadRunner，所以在正式开始讲解这个测试案例前，我会先给你简单介绍一下 LoadRunner 的基本原理，以及主要的功能模块。这些功能模块不仅在这个案例中会用到，也会在实际工程项目被经常使用，所以如果你有什么不理解的地方，欢迎给我留言。

LoadRunner 的基本原理

你还记得我在《工欲善其事必先利其器：后端性能测试工具原理与行业常用工具简介》这个主题中，介绍过的后端性能测试工具的基本原理吗？

我们先一起来回忆一下吧：

后端性能测试工具首先通过虚拟用户脚本生成器生成基于协议的虚拟用户脚本，然后根据性能测试场景设计的要求，通过压力控制器控制协调各个压力产生器以并发的方式执行虚拟用户脚本，并且在测试执行过程中，通过系统监控器收集各种性能指标以及系统资源占用率，最后通过测试结果分析器展示测试结果数据。

LoadRunner 的基本原理，与上面的描述完全一致。在 LoadRunner 中，Virtual UserGenerator 对应的就是虚拟用户脚本生成器，Controller

对应的就是压力控制器和系统监控器，Load Generator 对应的就是压力产生器，Analysis 对应的就是测试结果分析器。

为了帮助你理解 LoadRunner 的工作原理和模块，先撇开这些名词不谈，设想一下如果没有专用的后端性能测试工具，我们如何开展后端性能测试。

其实，“人肉”开展后端性能测试也不算太难。这个过程大致是这样的：

首先，我们需要一批测试机器，每台测试机器雇佣一个测试人员；

然后，我们需要一个协调员拿着话筒发号施令，统一控制这些测试人员的步调，协调员会向所有测试人员喊话，比如“1 号到 100 号测试人员现在开始执行登录操作，100 号到 1000 号测试人员 5 分钟后开始执行搜索操作”，同时协调员还会要求每个测试人员记录操作花费的时间；

测试完成后，测试协调员会要求性能工程师分析测试过程中记录的数据。

这个过程，如图 1 所示。

图 1 如果没有专用的后端性能测试工具，如何“人肉”开展后端性能测试

理解了这种“人肉”模式的后端性能测试后，我们再回过头来看 LoadRunner 的各个模块就豁然开朗了。

测试协调员以及完成数据记录的部分就是 Controller 模块；

大量的测试机器以及操作这些测试机器的人就是 Load Generator 模块；

操作这些测试机器的人的行为就是 Virtual User Generator 产生的虚拟用户脚本；

对测试数据的分析就是 Analysis 模块。

LoadRunner 的主要模块

通过对“人肉”模式和 LoadRunner 工具的类比，我们可以很清楚的看到，使用 LoadRunner 进行性能测试，主要需要 Virtual User Generator、Controller（这个模块包含了 Load Generator），以及 Analysis 这三大模块组合使用。接下来，我再和你详细聊聊这三大模块的作用，以及需要注意的问题。

第一，Virtual User Generator

Virtual User Generator，用于生成模拟用户行为的测试脚本，生成的手段主要是基于协议的录制，也就是由性能测试脚本开发人员在通过 GUI 执行业务操作的同时，录制客户端和服务器之间的通信协议，并最终转化为代码化的 LoadRunner 的虚拟用户脚本。

这样转化得到的虚拟脚本往往并不能被直接使用，还需要经历数据参数化（Parameterization）、关联建立（Correlation），以及运行时设置（Run Time Settings）等操作，然后才能用于性能测试场景中。

具体什么是数据参数化、什么是关联建立、运行时设置都有哪些可选项，我会在分享实例时再详细展开。

第二，LoadRunner Controller

Controller 相当于性能测试执行的控制管理中心，负责控制 Load Generator 产生测试负载，以执行预先设定好的性能测试场景；同时，它还负责收集各类监控数据。

在实际执行性能测试时，Controller 是和性能工程师打交道最多的模块，性能工程师会在 Controller 的 UI 界面上完成性能测试场景的设计、运行时的实时监控、测试负载的开始与结束等操作。

第三，LoadRunner Analysis

Analysis 是 LoadRunner 中一个强大的分析插件。它不仅能图形化展示测试过程中收集的数据，还能很方便地对多个指标做关联分析，找出它们之间的因果关系。它最根本的目的就是，分析出系统可能的性能瓶颈点以及潜在的性能问题。

现在，你已经了解了 LoadRunner 的原理和各个模块了，接下来我们就开始实战吧。通过这个实战，我希望你可以掌握如何基于 LoadRunner 进行企业级的性能测试。

从宏观角度来讲，基于 LoadRunner 完成企业级性能测试，可以划分为五个阶段：

性能需求收集以及负载计划制定；

录制并增强虚拟用户脚本；

创建并定义性能测试场景；

执行性能测试场景；

分析测试报告。

图 2 清晰地描述了这 5 个阶段的先后顺序，以及需要 LoadRunner 各模块发挥作用的部分。接下来，我和你详细聊聊每个阶段的具体工作，以及关键的技术细节。

图 2 使用 LoadRunner 完成企业级后端性能测试的典型流程与步骤

阶段 1：性能需求收集以及负载计划制定

其实，无论是进行什么类型的测试，你的第一步工作都是要根据测试目的明确测试的具体需求。企业级的后端性能测试，当然也不例外。

一般情况下，企业级后端性能测试的具体需求，主要包含以下内容：

系统整体的并发用户数。比如，高峰时段会有 10 万用户同时在线；

并发用户业务操作的分布情况。比如，20% 的用户在做登录操作，30% 的用户在做订单操作，其他 50% 的用户在做搜索操作；

单一业务操作的用户行为模式。比如，两个操作之间的典型停留时间，完成同一业务的不同操作路径等；

并发用户高峰期的时间分布规律。比如，早上 8 点会有大量用户登录系统，晚上 6 点后用户逐渐退出；

达到最高峰负载的时间长度。比如，并发用户从 0 增长到 10 万花费的总时间；

…

完成这些点的测试，其实并不复杂。你只要按照这个已经明确的需求，开发后续的测试脚本、设计性能测试场景就可以了。

但是，如果你想要成长为更资深的性能测试工程师，或者已经是性能测试的设计者、资深的性能测试工程师了，那么你就需要全程参与到这些需求的获取和确定中。

其实，在我看来，获取这些测试需求时性能测试中最难的两个工作之一。另一个最难的工作是，测试结果分析与性能问题定位。而其他类似性能测试脚本开发、场景设计等工作看起来很有技术含量，但实际都是一些相对机械性的重复工作。

那为什么获取测试需求难做呢？因为绝大多数情况下没人会明确告诉你具体的性能需求。

对于软件的功能测试来说，如果需求不明确，你可以直接求助于产品经理。

而对性能测试需求来讲，产品经理通常无法准确告诉你用户的各个业务操作所占的百分比，也无法告诉你准确的用户行为模式。产品经理能做的，往往是给出定性描述，然后需要你去计算或者根据过往经验得到具体的定量需求。所以，我们经常会听到产品经理对性能测试人员说：“你是性能专家，你来告诉我性能需求”。

那么，对于性能测试设计人员来说，到底如何获得这个明确的性能需求呢？说到这里，你应该明白了这是一个非常复杂的话题，因为测试目的不同，所用的方法也各不相同。所以，在这次分享中，我也只是可以给你准备一个实际的测试案例，和你分享获取具体测试需求的思考方式。

还记得我在第 29 篇文章《聊聊性能测试的基本方法与应用领域》中介绍的医院体检的例子吗？假设，产品经理对医院体检的性能要求是“每天支持完成 8000 个体检”，这个需求看似很具体，但是要转化成实际可操作的性能测试需求，还需要再细化。

首先，你要明确这里的“每天”是否指的是 24 小时。显然，这取决于产品本身的属性。比如，产品是为单一时区的用户提供服务，还是要面向全球所有时区的用户。那么，根据体检中心的属性，你很容易就可以确定“每天”一定是指 8 小时的工作时间。因为，体检中心一定是在一个确定的时区，并且不会 24 小时营业。

然后，你明确了这个 8 小时后，那么原始需求是不是可以转化为“每小时支持完成 1000 个体检”？

如果按照这个套路设计后续的性能测试的话，你会发现即使测试顺利完成，并且各项性能指标都达标了，但是一旦上线后，系统还是很有可能被压垮。因为实际情况是，验血往往需要空腹，所以上午往往是体检中心的高峰时段，体检者会在上午集中涌入体检中心。也就是说，这 8000 个体检并不是平均分布在 8 小时内完成的，而是有明显的高峰时段。

最后，你可以采用 80/20 原则对高峰时段的用户负载进行建模，比如 80% 的体检（6400 个）是发生在上午 20% 的时间（96 分钟）里。当然，为了使模型更接近真实的情况，你还应该分析历史数据，然后对该模型做进一步的修正，这也是目前被普遍采用的方法。

另外，在得到了负载模型后，性能测试设计人员往往还会在此基础上加入一定的负载冗余，比如在峰值的基础上再额外放大 20%，以增强系统上线后稳定运行的信心。

通过上面这个分析过程，你可以认识到，性能测试需求的定义与计划非常复杂，牵涉到项目的方方面面，不可能通过阅读一两篇文章就快速掌握这一技能，需要不断地沉淀在实战中获得的经验。

制订了性能测试计划后，接下来你就需要根据性能计划中涉及的用户业务操作来开发性能测试的脚本了。比如，前面提到“20% 的用户在做登录操作，30% 的用户在做订单操作，剩下 50% 的用户在做搜索操作”，接下来你需要分别开发“用户登录”“下订单”和“搜索”这三个虚拟用户脚本。

在 LoadRunner 中，开发虚拟用户脚本的工作主要是基于录制后再修改的方式完成的。其中，录制由 Virtual UserGenerator 基于协议完成，录制后的修改主要是实现参数化、建立关联、建立事务、加入必要的检查点以及加入思考时间。

所以，我会在下次分享时，和你详细讨论这四部分工作的作用，以及具体如何完成。同时，我还会和你分享，企业级服务器端性能测试的后四个阶段如何实现。

总结

今天我和你讨论的主题是，如何基于 LoadRunner 实现企业级服务器端性能测试。

首先，我用一个“人肉”测试的流程类比，为你介绍了 LoadRunner 这个工具的基本原理，并分析了 Virtual User Generator、Controller（内含 Load Generator 模块），以及 Analysis 这 3 个模块的功能。

然后，从宏观角度，我把整个性能测试过程划分成了五个阶段：性能需求收集以及负载计划制定、录制并增强虚拟用户脚本、创建并定义性能测试场景、执行性能测试场景，以及分析测试报告。

在我看来，这五个阶段最难的两部分工作分别是：明确具体的性能测试需求，以及测试结果分析与性能问题定位。因为这两部分工作，要大量依赖于测试工程师的能力以及经验积累。所以，就像一名优秀的医生一样，优秀的测试工程师，需要在实际的工程项目中不断积累和总结经验。

最后，我以前面文章提到过的体检中心为例，和你详细讨论了如何收集性能需求，以及制定负载计划的内容。我也解释了为什么性能测试的需求不能直接从产品经理那里获得：因为产品经理定义的性能需求层次比较高、比较抽象，要落实到实际可执行的性能测试需求往往需要分析和细化。这也是为什么获取具体的性能需求比较难的一个原因。

思考题

你在实际工作中，是如何获取并细化性能测试需求的呢？

感谢你的收听，欢迎你给我留言。

33 | 无实例无真相：基于LoadRunner实现企业级服务器端性能测试的实践（下）

你好，我是茹炳晟。今天我和你分享的主题是：无实例无真相之基于 LoadRunner 实现企业级服务器端性能测试的实践（下）。

今天，我会继续和你分享如何基于 LoadRunner 完成企业级服务器端的性能测试。通过我上一次的分享，你已经清楚知道了，整个性能测试过程可以分为五个阶段，并且解决了整个测试过程中最难的一部分工作，即如何获取具体的性能测试需求。

现在，我们先来回顾一下，性能测试包含的五个阶段：性能需求收集以及负载计划制定、录制并增强虚拟用户脚本、创建并定义性能测试场景、执行性能测试场景，以及分析测试报告。所以，今天，我们就要解决剩下的 4 个阶段的问题了。

阶段 2：录制并增强虚拟用户脚本

我已经在上篇文章中和你提到，完成了性能测试需求分析后，你就已经明确了要开发哪些性能测试脚本。现在，我们就一起来看看开发性能测试脚本的步骤，以及相关的技术细节。

从整体角度来看，用 LoadRunner 开发虚拟用户脚本主要包括以下四个步骤：

识别被测应用使用的协议；

录制脚本；

完善录制得到的脚本；

验证脚本的正确性。

这里需要注意的是，完善录制得到的脚本这一步，会包含大量的技术细节，也有很多对你来说可能是新概念的名词，所以我会着重讲解这一步，帮你克服性能测试道路上的这些“拦路虎”。

步骤 1：识别被测应用使用的协议

如果你已经和系统设计、开发人员沟通过，明确知道了被测系统所采用的协议，那么你可以跳过这一步。如果还不知道具体使用的哪种协议的话，你可以使用 Virtual User Generator 模块自带的 Protocol Advisor 识别被测应用使用的协议，具体的操作方法也很简单：

在 Virtual User Generator 中依次点击 File、Protocol、AdvisorAnalyze、Application，展开这些菜单。

在打开的界面上按要求填写被测应用的信息。

Protocol Advisor 会自动运行被测系统。如果是网页应用，就会打开浏览器。

在页面上执行一些典型的业务操作，完成这些业务操作后点击 "Stop Analyzing" 按钮停止录制。

Protocol Advisor 会根据刚才录制的内容自动分析被测应用使用的协议，并给出最终的建议。

接下来，你就可以使用 Protocol Advisor 建议的录制协议开始脚本录制工作了。如图 1 所示就是 Protocol Advisor 给出的建议录制协议界面。

图 1 Protocol Advisor 给出的建议录制协议界面

步骤 2：录制脚本

脚本录制的基本原理是，通过 GUI 界面对被测系统进行业务操作，Virtual User Generator 模块在后台捕获 GUI 操作所触发的客户端与服务器端的所有交互，并生产基于 C 语言的虚拟用户脚本文件。

也就是说，录制脚本的过程需要通过 GUI 实际执行业务操作，所以我建议你在开始录制脚本前，先多次演练需要这些 GUI 操作步骤，并明确知道哪些操作步骤会对服务器端发起请求。

我们要知道哪些操作步骤会对服务器发起请求的原因是，要将这些操作步骤在虚拟用户脚本中封装成“事务”（Transaction）。封装为“事务”的目的是统计响应时间，因为 LoadRunner 中的响应时间都是以“事务”为单位的。

具体的录制步骤，主要包括如下三步，

首先，选择 Create/Edit Scripts 进入 Virtual User Generator 创建脚本的协议选择界面。

选择正确的协议后进入 Start Recording 界面，选择需要录制的应用类型，并填写应用的详细信息。如果是 Web 应用，Application type 就应该选择 Internet Application，然后选择浏览器并填写这个 Web 应用的 URL，完成后自动打开浏览器。

在该浏览器中执行业务操作，Virtual User Generator 模块会记录所有的业务操作，并生成脚本。

在录制脚本的过程中，我强烈建议直接对发起后端调用的操作添加事务定义，而不要等到脚本生成后再添加。因为 LoadRunner 脚本的可读性并不好，在录制完的脚本中添加事务定义的难度会很大。

在录制过程中，直接添加事务操作也很简单，主要包括如下三步：

在开始执行 GUI 操作前，先点击图 2 中的“事务开始”按钮并填写事务名称；

执行 GUI 操作；

操作完成后，点击图 2 中的“事务结束”按钮。

这样你刚才执行 GUI 操作的脚本就会被 lr_start_transaction(“事务名称”) 和 lr_end_transaction(“事务名称”，LR_AUTO) 包围起来，也就完成了添加事务的定义。

图 2 Virtual User Generator 的脚本录制控制条

步骤 3：完善录制得到的脚本

脚本录制，只是虚拟用户脚本开发中最简单的一步。我在上一次分享《无实例无真相：基于 LoadRunner 实现企业级服务器端性能测试的实践（上）》时，提到由 Virtual User Generator 模块录制的脚本不能直接使用，我们还需要对录制的脚本做以下处理：

在两个事务之间加入思考时间（Think Time）；

对界面输入的数据做参数化（Parameterization）操作；

完成脚本的关联（Correlation）操作；

加入检查点（Check Point）。

这 4 步处理操作是虚拟用户脚本开发中最关键的地方，你不仅需要知道为什么要进行这些处理，更要能够完成这些处理，否则你录制的脚本无法成功回放。

第一，在两个事务之间加入思考时间

什么是思考时间呢？

用户在实际使用系统时，并不会连续不断地向后端服务器发起请求，在两次发起请求之间往往会有一个时间的间隔，这个时间间隔主要来自于两个方面：

一是，用户操作的人为等待时间，因为用户不可能像机器人那样快速地执行操作；

二是，用户可能需要先在页面上填写很多信息后之后，才能提交操作，那么填写这些信息就需要花费一定的时间。

所以，为了让虚拟用户脚本能够更真实地模拟实际用户的行为，我们就需要在两个事务之间加入一定的等待时间。这个等待时间，就是 LoadRunner 中的思考时间。

你只要直接调用 LoadRunner 提供的 lr_think_time() 函数，就可以在两个事务之间加入思考时间。但是，这个思考时间到底设置为多少，并没有那么容易知道。思考时间往往会涉及多方面的因素，严格计算的话会非常复杂。

所以，在实际项目中，一般先粗略估计一个值（比如 15 s），然后在实际执行负载场景的过程中，再根据系统吞吐量调整。

你在后续调整思考时间时，无需逐行修改虚拟用户脚本代码，可以在 Run-time Settings（运行时设置）中很方便地完成。如图 3 所示，Run-time Settings 中支持多种方式调整思考时间。

图 3 通过 Run-time Settings 统一调整思考时间

As recorded，代表的是直接使用 lr_think_time() 函数中指定的时间。

Mutiply recorded think time by，代表的是在 lr_think_time() 函数中指定的时间基础上乘以一个数字。比如这个数字是 2，那么所有的思考时间都会翻倍。

Use random percentage of recorded think time，指的是使用指定思考时间范围内的随机值。例如，如果 lr_think_time() 函数中指定的时间是 2 s，并且指定最小值为 50%，最大值为 200%，则实际的思考时间会取最小值 1 s（2 s50%）和最大值 4 s（2 s200%）之间的随机值。

Limit think time to，指的是为思考时间设置一个上限值，只要 lr_think_time() 函数中指定的时间没有超过这个上限值，就按照 lr_think_time() 函数指定的值，如果超过了就取这个上限值作为思考时间。

第二，对界面输入的数据做参数化操作

数据的参数化，其实很好理解，我再给你举个例子，你马上就能明白。

假设，你录制的虚拟用户脚本完成的是用户登录操作，那么由于脚本回放时需要支持多用户的并发，所以必须要把脚本中的用户名和密码独立出来，放入专门的数据文件中，然后在这个文件中提供所有可能被用到的用户名和密码。

有没有感觉这个概念很熟悉，它其实和我以前介绍到的数据驱动的自动化测试完全相同。

图 4 给出了参数化配置的界面截图，LoadRunner 支持的参数化的数据源很丰富，既可以是 excel 文件，也可以是数据库中的表等。

图 4 虚拟用户脚本参数化配置的界面截图

这里需要特别说明的是，凡是参数文件中使用的测试数据都需要在执行性能测试前，在被测系统中事先准备好。比如，还是以用户登录的脚本为例，假定你的参数文件中提供了 5000 个用于并发执行的用户信息，那么这 5000 个用户必须是已经实际存在于系统中的，这就要求你要在开始测试前事先准备好这 5000 个用户。

所以，参数化操作其实由两部分组成：

性能测试脚本和测试数据的分离；

事先建立性能测试的数据。

也就是说，参数化的过程往往与性能测试数据准备密不可分。

第三，完成脚本的关联操作

关联操作，是 LoadRunner 虚拟用户脚本开发过程中最关键的部分，直接关系到脚本是否可以回放成功。

从概念上讲，关联的主要作用是，取出前序调用返回结果中的某些动态值，传递给后续的调用。是不是听起来很拗口，不太好理解？我们来看一个具体的例子吧。

假设，每次客户端连接服务器端时，服务器端都会用当前的时间戳（Time Stamp）计算 CheckSum，然后将 Time Stamp 和 CheckSum 返回给客户端。然后，客户端就把 Time Stamp + CheckSum 的组合作为唯一标识客户端的 Session ID。录制脚本时，录制得到的一定是硬编码（hardcode）的 Time Stamp 值和 CheckSum 值。

图 5 展示了这个交互过程，录制得到 Time Stamp 的值是 TS，而 CheckSum 的值是 CS。

图 5 关联原理图 - 脚本录制过程

采用 Time Stamp + CheckSum 的组合作为 Session ID 的方式，在我们回放这个脚本的时候就有问题了。因为回放时，这段硬编码已经有了新的 Time Stamp 值和 CheckSum 值，并且显然与之前的值不同，所以服务器无法完成 Session ID 的验证，也就导致了脚本回放失败。

图 6 关联原理图 - 脚本回放过程

其实，这种情况几乎存在于所有的虚拟用户脚本中，所以我们必须要解决这个问题。

解决方法就是，在脚本回放的过程中，实时抓取 Time Stamp 值和 CheckSum 值，然后用实时抓取到的值替换后续需要使用这两个值的地方。这个过程就是“关联”。

如图 7 所示，关联就是解析服务器端的返回结果，抓取新的 Time Stamp 值和 CheckSum 值，然后后续的操作都使用新抓取的值，这样脚本就能回放成功了。

图 7 关联原理图 - 使用“关联”后的脚本回放过程

理解了关联操作，在脚本中处理关联就比较简单了，LoadRunner 提供了功能强大的关联函数 web_reg_save_param()。这个关联函数支持多种动态值的获取方式，用得最多的是基于“前序字符串匹配”加上“后续字符串匹配”的方式。其中，字符串匹配，支持正则表达式。

我们一起来看个具体的例子吧。

假设，服务器端返回的结果是“LB=name=timestamp value=8888.LB=name=CheckSum”，那么为了能够获取到“8888”这个动态值，我们就可以用“前序字符串 =LB=name=timestamp value=”和“后续字符 =.LB=name=CheckSum”来“框出” 8888”这个动态值。

另外，需要特别注意的是 web_reg_save_param() 函数是注册型函数，必须放在获取动态值所属的请求前面，相当于先声明，后调用。

更多的关联函数用法，你可以参考 LoadRunner 官方文档。

第四，加入检查点

检查点，类似于功能测试中的断言。但是，性能测试脚本，不像功能测试脚本那样需要加入很多的断言，往往只在一些关键步骤后加入很少量的检查点即可。这些检查点的主要作用是，保证脚本按照原本设计的路径执行。

最常用的检查点函数是 web_reg_find()，它的作用是通过指定左右边界的方式“在页面中查找相应的内容”。这里需要注意的是，这个函数也是注册型函数，即需要放在所检查的页面之前，否则会检查失败。更多的检查点函数以及用法也请参考 LoadRunner 官方文档。

步骤 4：验证脚本的正确性

完成了脚本开发后，根据我的个人经验，我强烈建议你按照以下顺序检查脚本的准确性：

以单用户的方式，在有思考时间的情况下执行脚本，确保脚本能够顺利执行，并且验证脚本行为以及执行结果是否正确；

以单用户的方式，在思考时间为零的情况下执行脚本，确保脚本能够顺利执行，并且验证脚本行为以及执行结果是否正确；

以并发用户的方式，在有思考时间的情况下执行脚本，确保脚本能够顺利执行，并且验证脚本行为以及执行结果是否正确；

以并发用户的方式，在思考时间为零的情况下执行脚本，确保脚本能够顺利执行，并且验证脚本行为以及执行结果是否正确。

只有上述四个测试全部通过，虚拟用户脚本才算顺利完成。

至此，我们完成了第二个阶段的“录制并增强虚拟用户脚本”的工作，顺利拿到了虚拟用户脚本。那么接下来，我们就会进入第三个阶段，使用开发完成的虚拟用户脚本创建并定义性能测试场景。

阶段 3：创建并定义性能测试场景

还记得我在分享《工欲善其事必先利其器：后端性能测试工具原理与行业常用工具简介》这个主题时，介绍过的性能测试场景的内容吗？如果有点忘记了，我建议你先回顾一下这篇文章的内容。

这个阶段的工作，就是在 LoadRunner Controller 中设置性能测试场景。由于整个设置过程，都是基于 Controller 的图形用户界面的操作，本身没什么难度，所以我就不再详细展开了，如果有这方面的问题，你也可以自行百度或者给我留言。

阶段 4：执行性能测试场景

完成了性能测试场景的设计与定义后，执行性能测试场景就非常简单了。

这个过程一般是在 LoadRunner Controller 中完成。你可以通过 Controller 发起测试、停止测试、调整性能测试场景的各种参数，还可以监控测试的执行过程。

阶段 5：分析测试报告

执行完性能测试后，LoadRunner 会根据自己的标准并结合性能测试场景中定义的系统监控器指标，生成完整的测试报告。在 Analysis 中，不仅可以以图形化的方式显示单个指标，也可以将多个指标关联在一起进行比较分析。

图 8 展示了使用 LoadRunner Analysis 展示事务平均响应时间的界面，我们可以看到图片右下角各个事务的最小响应时间、最大响应时间和平均响应时间。

图 8 性能测试报告的分析

性能测试报告的分析，是一项技术含量非常高的工作。优秀的性能测试工程，通过报告中的数值以及数值之间的相互关系，就能判断出系统中可能存在的问题。这就好比医生看验血报告，经验丰富的医生可以根据验血报告对病情做出八九不离十的判断。

性能测试报告的解读，需要丰富的系统架构、性能理论以及大量实战经验的积累。这个话题已经超出了我今天要分享的范围，所以我也就不再继续展开了。

总结

今天接着上一篇文章，我和你分享了企业级后端性能测试的后四个阶段的内容，包括录制并增强虚拟用户脚本、创建并定义性能测试场景、执行性能测试场景，以及分析测试报告。现在，我再为你总结一下每一个阶段的重点内容。

录制并增强虚拟用户脚本，这个阶段的工作又可以分为识别被测应用使用的协议、录制脚本、完善录制得到的脚本、验证脚本的正确性四步。其中，完善录制得到的脚本这一步，涉及到了很多概念和基础知识，所以我进行了重点讲解，希望帮你克服性能测试的难点。

创建并定义性能测试场景，以及执行性能测试场景，这两个阶段的工作都是在 LoadRunner 的 Controller 模块中完成的，也都比较简单。你可以参考我在《工欲善其事必先利其器：后端性能测试工具原理与行业常用工具简介》这篇文章分享的内容，完成这两个阶段的工作。

分析测试报告，这个工作的技术含量非常高。深入解读性能测试报告的能力，需要丰富的系统架构、性能理论，以及大量实战经验。所以，我们需要在平时工作中，不断地丰富自己的知识体系。

思考题

你们公司的性能测试是否使用 LoadRunner，在使用过程中遇到了什么难题？你们又是如何解决的呢？

感谢你的收听，欢迎你给我留言。

34 | 站在巨人的肩膀：企业级实际性能测试案例与经验分享

你好，我是茹炳晟，今天我分享的主题是：站在巨人的肩膀之企业级实际性能测试案例与经验分享”。

在前面的四篇文章中，我介绍了前端性能测试和后端性能测试的理论与方法，还分享了使用 LoadRunner 实现后端性能测试的过程。有了这些内容的铺垫，我今天会和你聊聊传统的企业级软件企业如何开展性能测试工作。

其实，传统的企业级软件产品和互联网产品的性能测试，在原理和测试方法上基本一致，它们最大的区别体现在并发数量的数量级上，以及互联网软件产品的性能测试还需要直接在生产环境下进行特有的全链路压测。而全链路压测其实是在传统的企业级软件产品的性能测试基础上，又进行了一些扩展。

所以，在我看来，只要掌握了传统的企业级软件产品的性能测试的原理和方法，搞定互联网产品的性能测试自然不在话下。

言归正传，传统企业级软件产品的性能测试重点是在服务器端。为了达到不同的测试目标，往往会有多种不同类型的性能测试。今天，我就和你聊聊这其中都有哪些测试类型，以及每类测试的目的、所采用的方法。

所以，今天的分享，我会从以下四种测试类型的角度展开：

性能基准测试；

稳定性测试；

并发测试；

容量规划测试。

性能基准测试

性能基准测试，通常被称为 Performance Benchmark Test，是每次对外发布产品版本前必须要完成的测试类型。

性能基准测试，会基于固定的硬件环境和部署架构（比如专用的服务器、固定的专用网络环境、固定大小的集群规模、相同的系统配置、相同的数据库背景数据等），通过执行固定的性能测试场景得到系统的性能测试报告，然后与上一版本发布时的指标进行对比，如果发现指标有“恶化”的趋势，就需要进一步排查。

典型的“恶化”趋势，主要表现在以下几个方面：

同一事务的响应时间变慢了。比如，上一版本中，用户登录的响应时间是 2 s，但是在最新的被测版本中这个响应时间变成了 4 s；

系统资源的占用率变高了。比如，上一版本中，平均 CPU 占用率是 15%，但是在最新的被测版本中平均 CPU 占用率变成了 30%；

网络带宽的使用量变高了。比如，上一版本中，发送总字节数是 20 MB，接收总字节数是 200 MB，但是在最新的被测版本中发送总字节数变成了 25 MB，接收总字节数变成了 250 MB。

这里需要注意的是，这些“恶化”趋势的前提是：完全相同的环境以及测试负载。不同“恶化”指标的排查，有不同的方法。我以最常见的事务响应时间变慢为例，和你说明一下排查方法。

假设，通过性能基准测试的比较结果得知，用户登录的响应时间从 2 s 变成了 4 s。

那么，我们首先要做的是验证在单用户的情况下，是否会出现响应时间变长的问题。具体做法是，将用户登录的虚拟用户脚本单独拿出来，建立一个单用户运行的性能测试场景并执行，观察用户登录的响应时间是否变慢。

如果变慢了，就说明这是单用户登录场景就可重现的性能问题，后续的处理也相对简单了。解决方法是：分析单用户登录的后端日志文件，看看完成登录操作的时间具体都花在了哪些步骤上，相比之前哪些步骤花费的时间变长了，或者是多出了哪些额外的步骤。

如果没有变慢，则说明我们必须尝试在有压力的情况下重现这个性能问题。为此，我们要基于用户登录的虚拟用户脚本构建并发测试的场景，但是我们并不清楚在这个场景设计中到底应该采用多少并发用户、加入多长的思考时间。这时，通常的做法是，直接采用性能基准测试中的并发用户数和思考时间，去尝试重现问题。如果无法重现，我们可以适当地逐步加大测试负载，并观察响应时间的变化趋势。

这里需要注意的是，千万不要使用过大的测试负载。因为测试负载过大的话，系统资源也会成为性能瓶颈，一定会使响应时间变长。但这时，响应时间变长主要是由资源瓶颈造成的，而不是你开始要找的那个原因。

如果此时可以重现问题，那就可以进一步去分析并发场景下，用户登录操作的时间切片，找到具体的原因。如果此时还是不能重现问题的话，情况就比较复杂了，也就是登录操作的性能可能和其他的业务操作存在依赖，或者某种资源竞争关系，这就要具体问题具体分析了。

一般来说，当定位到性能“恶化”的原因并修复后，我们还会再执行一轮性能基准测试，以确保系统对外发布前的性能基准测试指标没有“变坏”。可以说，通过对每个预发布版本的性能基准测试，我们可以保证新发布系统的整体性能不会下降，这也就是性能基准测试最终要达到的目的。

很多大型的传统软件公司都有专门的性能测试团队，这个团队会建立标准的性能基准测试场景，并把性能基准测试的结果作为产品是否可以发布的依据之一。比如，我曾工作过的 HP 软件，就由性能测试卓越中心负责维护、执行性能基准测试，并分析测试结果。

从性能基准测试的设计角度来看，你需要特别注意以下三点：

性能基准测试中虚拟用户脚本的选择以及配比，需要尽可能地匹配实际的负载情况；

总体的负载设计不宜过高，通常被测系统的各类占用率指标需要控制在 30% 以内，尽量避免由于资源瓶颈引入的操作延时；

每次性能基准测试前，一般需要对系统资源以及网络资源做一轮快速的基准测试，以保证每次被测环境的一致性，同时也要保证数据库的数据量在同一个级别上。总之，你需要采用一切可能的手段，来确保多次性能基准测试之间的环境一致性。

稳定性测试

稳定性测试，又称可靠性测试，主要是通过长时间（7*24 小时）模拟被测系统的测试负载，来观察系统在长期运行过程中是否有潜在的问题。通过对系统指标的监控，稳定性测试可以发现诸如内存泄漏、资源非法占用等问题。

很多企业级的服务器端产品，在发布前往往都要进行稳定性测试。稳定性测试，通常直接采用性能基准测试中的虚拟用户脚本，但是性能测试场景的设计和性能基准测试场景会有很大不同：

一般是采用“波浪式”的测试负载，比如先逐渐加大测试负载，在高负载情况下持续 10 多个小时，然后再逐渐降低负载，这样就构成了一个“波浪”，整个稳定性测试将由很多个这样的波浪连续组成。

稳定性测试成功完成的标志，主要有以下三项：

系统资源的所有监控指标不存在“不可逆转”的上升趋势；

事务的响应时间不存在逐渐变慢的趋势；

事务的错误率不超过 1%。

实际工程项目中，由于稳定性测试执行的时间成本很高，往往需要花费 3~7 天的时间，所以我们一般是在其他所有测试都已经完成，并且所有问题都已经修复之后才开始稳定性测试。

另外，有些企业为了缩短稳定性测试的执行时间，往往还会采用“时间轴压缩”的方法，具体的做法就是：在加大测试负载的前提下，适当缩短每个“波浪”的时间，从而减少整体的测试执行时间。

最后，需要强调的一点是，虽然很多时候，尤其是产品版本已经逐渐走向成熟期时，稳定性测试并不会发现问题，但是千万不要小看稳定性测试带来的价值。因为稳定性测试一旦发现问题，那么这些问题都是很严重而且非常隐蔽的大问题。

所以，很多大型的企业级软件企业都会执行严格的稳定性测试，并把稳定性测试的结果作为产品是否可以发布的硬性要求。比如，我曾经工作过的 HP 软件研发中心，它每次产品发布前都会由专门的性能测试团队完成严格的稳定性测试，并以此来决定是否要发布这个产品。

并发测试

并发测试，是在高并发情况下验证单一业务功能的正确性以及性能的测试手段。高并发测试一般使用思考时间为零的虚拟用户脚本来发起具有“集合点”的测试。

“集合点”的概念，我已经在《聊聊性能测试的基本方法与应用领域》中解释过了。如果你不清楚的话，可以再回顾一下这篇文章。如果你还有不理解的地方，也欢迎和我留言讨论。

并发测试，往往被当作功能测试的补充，主要用于发现诸如多线程、资源竞争、资源死锁之类的错误。要执行并发测试，就需要加入“集合点”，所以往往需要修改虚拟用户脚本。

加入“集合点”一般有两种做法：

在虚拟用户脚本的录制过程中直接添加；

在虚拟用户脚本中，通过加入 lr_rendezvous() 函数添加。

容量规划测试

容量规划测试，是为了完成容量规划而设计执行的测试。

那什么是容量规划呢？所谓容量规划，是软件产品为满足用户目标负载而调整自身生产能力的过程。

所以，容量规划的主要目的是，解决当系统负载将要达到极限处理能力时，我们应该如何通过垂直扩展（增加单机的硬件资源）和水平扩展（增加集群中的机器数量）增加系统整体的负载处理能力的问题。

目前来讲，容量规划的主要方法是基于水平扩展。但是，具体应该增加多少机器，以及增加后系统的负载处理能力是否会线性增长，这些问题都需要通过容量规划测试进行验证。

那么，容量规划测试具体要怎么做呢？

我们可以使用性能基准测试中的虚拟用户脚本，以及各个业务操作脚本的百分比，压测单机部署的被测系统。我们会采用人工的方式不断增加测试负载直到单机系统的吞吐量指标到达临界值，由此就可以知道单台机器的处理能力。

理论上讲，整个集群的处理能力将等于单台机器的处理能力乘以集群的机器数，但是实际情况并不是这样。实际的集群整体处理能力一定小于这个值，但具体小多少就是要靠实际的测试验证了。

理想的状态是，集群整体的处理能力能够随着集群机器数量的增长呈线性增长。但是，随着机器数量的不断增长，总会在达到某个临界值之后，集群的整体处理能力不再继续呈线性增长。这个临界值是多少，我们也需要通过容量规划测试找出来了。

另外，容量规划测试的测试结果还可以被用作系统容量设计的依据。比如，企业级软件产品的目标用户规模通常是可以预估的，那么我们就可以通过这些预估的系统负载计算出软件部署的集群规模，并且可以在具体实施后通过容量测试的方式进行验证。

总结

在前面的两篇文章中，我和你分享了如何基于 LoadRunner 开展性能测试，但是并没有具体去讲解要开展哪些类型的性能测试。所以，今天我就挑选了最重要的四类性能测试方法，和你分享如何在实际项目中完成这些测试，确保软件的性能。

性能基准测试，可以保证新发布系统的整体性能不会下降；

稳定性测试，主要通过长时间模拟被测系统的测试负载，观察系统在长期运行过程是否存在问题；

并发测试，往往被当作功能测试的补充去发现多线程、资源竞争、资源死锁之类的问题。

容量规划测试，主要用于确定给定负载下的系统集群规模，其测试结果可以被用作系统容量设计的依据。

思考题

你所在企业，还会采用哪些性能测试方法，又是如何展开具体的测试工作的呢？

感谢你的收听，欢迎你给我留言。

35 | 如何准备测试数据？

你好，我是茹炳晟，今天我和你分享的主题是：如何准备测试数据。

从今天开始，我们将一起进入测试数据准备这个新的系列了。我会用四篇文章，和你详细探讨软件测试过程中关于测试数据准备的话题。我会依次分享测试数据创建的基本方法、测试数据准备的痛点、自行开发的测试数据工具，以及目前业内最先进的统一测试数据平台。

你我都非常清楚，测试数据的准备是软件测试过程中非常重要的一个环节，无论是手工测试，还是自动化测试，无论是 GUI 测试，还是 API 测试，无论是功能测试，还是性能测试，都避不开测试数据准备的工作。

所以，如果你想要成长为一名优秀的测试工程师，那就非常有必要深入理解测试数据准备的方法，以及它们各自的优缺点、适用场景了。

今天，我们就先从测试数据准备的基本方法开始吧。

从创建测试数据的维度来看，测试数据准备方法主要可以分为四类：

基于 GUI 操作生成测试数据；

通过 API 调用生成测试数据；

通过数据库操作生成测试数据；

综合运用 API 和数据库的方式生成测试数据。

这时，相信你已经回想起我曾在第 15 篇文章《过不了的坎：聊聊 GUI 自动化过程中的测试数据》中从创建测试数据的维度和你分享过这些内容，这次的分享只不过是多了“通过 GUI 调用生成测试数据”的方法。

其实，我在第 15 篇文章的分享内容，只是简单的介绍了 GUI 测试数据准备的方法，并没有详细展开。事后，你可能也感觉不太过瘾，想知道一些更深入、更细节、更贴近业务场景的测试数据准备的知识。所以，也就有了我今天的这次分享。

那么，接下来我们就赶紧开始吧，一起聊聊这四种测试数据准备的方法。

基于 GUI 操作生成测试数据

基于 GUI 操作生成测试数据，是最原始的创建测试数据的方法。简单地说，它就是采用 E2E 的方式来执行业务场景，然后生成数据的方法。

比如，你想要测试用户登录功能，那么首先就要准备一个已经注册的用户，为此你可以直接通过 GUI 界面来注册一个新用户，然后用这个新创建的用户完成用户登录功能的测试。

这个方法的优点是简单直接，在技术上没有任何复杂性，而且所创建的数据完全来自于真实的业务流程，可以最大程度保证数据的正确性。但是，该方法的缺点也十分明显，主要体现在以下这四个方面：

创建测试数据的效率非常低。一是因为每次执行 GUI 业务操作都只能创建一条数据，二是因为基于 GUI 操作的执行过程比较耗时。

基于 GUI 的测试数据创建方法不适合封装成测试数据工具。由于测试数据的创建是通过 GUI 操作实现的，所以把这种数据创建方法封装成测试数据准备工具的过程，其实就是在开发 GUI 自动化测试用例。无论是从开发工作量，还是从执行效率来讲，把基于 GUI 操作的测试数据创建方法封装成测试数据准备工具都不是最佳的选择。

测试数据成功创建的概率不会太高。因为，测试数据准备的成功率受限于 GUI 自动化执行的稳定性，而且任何界面的变更都有可能引发测试数据创建的失败。

会引入不必要的测试依赖。比如，你的被测对象是用户登录功能，通过 GUI 页面操作准备这个已经注册的用户，就首先要保证用户注册功能没有问题，而这显然是不合理的。

鉴于以上四方面的原因，在实际的测试过程中，我们很少直接使用基于 GUI 的操作生成测试数据。只有在万不得已的情况下，比如没有其他更好的方式可以创建正确可靠的测试数据时，我们才会使用这个方法。

而且，这里我需要说明的是，基于 GUI 操作生成测试数据的方法一般只用于手工测试，因为自动化测试中使用这种数据准备方法，基本相当于要开发一个完整的 GUI 自动化测试用例，代价太大。

那我为什么还要介绍这个方法呢？其实，这个方法更重要的应用场景是，帮助我们找到创建一个测试数据的过程中，后端调用了哪些 API，以及修改了哪些数据库的业务表，是“通过 API 调用生成测试数据”，以及“通过数据库操作生成测试数据”这两种方法的基础。

通过 API 调用生成测试数据

通过 API 调用生成测试数据，是目前主流的测试数据生成方法。其实，当我们通过操作 GUI 界面生成测试数据时，实际的业务操作往往是由后端的 API 调用完成的。所以，我们完全可以通过直接调用后端 API 生成测试数据。

还是以用户登录功能的测试为例，我们通过 GUI 界面注册新用户时，实际上就是调用了 createUser 这个 API。既然知道了具体要调用哪个 API，那么我们就可以跳过在 GUI 界面的操作，直接调用 createUser 生成“已经注册的用户”这个测试数据了。

为了规避在创建测试数据时过于在乎实现细节的问题，在实际工程实践中，我们往往会把调用 API 生成测试数据的过程封装成测试数据准备函数。那问题是，我怎么才能知道前端新用户注册这个操作到底调用了哪些后端 API 呢？这里，我推荐三种方式：

直接询问开发人员，这是最直接的方法；

如果你有一定的代码基础，可以直接阅读源代码，这个方法也可以作为直接询问方法的补充；

在一个你可以独占的环境上执行 GUI 操作创建测试数据，与此同时监控服务器端的调用日志，分析这个过程到底调用了哪些 API。

通过 API 调用生成测试数据的方法，优点主要体现在以下几个方面：

可以保证创建的测试数据的准确性，原因是使用了和 GUI 操作同样的 API 调用；

测试数据准备的执行效率更高，因为该方法跳过了耗时的 GUI 操作；

把创建测试数据的 API 调用过程，封装成测试数据函数更方便，因为这个调用过程的代码逻辑非常清晰；

测试数据的创建可以完全依赖于 API 调用，当创建测试数据的内部逻辑有变更时，由于此时 API 内部的实现逻辑也会由开发人员同步更新，所以我们依旧可以通过调用 API 来得到逻辑变更后的测试数据，而这个过程对使用来说是完全透明的。

但是，该方法也不是完美无瑕的，其缺点主要表现在：

并不是所有的测试数据创建都有对应的 API 支持。也就是说，并不是所有的数据都可以通过 API 调用的方式创建，有些操作还是必须依赖于数据库的 CRUD 操作。那么，这时，我们就不得不在测试数据准备函数中加入数据库的 CRUD 操作生成测试数据了。

有时，创建一条业务线上的测试数据，往往需要按一定的顺序依次调用多个 API，并且会在多个 API 调用之间传递数据，这也无形中增加了测试数据准备函数的复杂性。

虽然相比于 GUI 操作方式，基于 API 调用的方式在执行速度上已经得到了大幅提升，并且还可以很方便地实现并发执行（比如，使用 JMeter 或者 Locust），但是对于需要批量创建海量数据的场景，还是会力不从心。

因此，业界往往还会通过数据库的 CRUD 操作生成测试数据。

通过数据库操作生成测试数据

通过数据库操作生成测试数据，也是目前主流的测试数据生成方法。这个方法的实现原理很简单，就是直接通过数据库操作，将测试数据插入到被测系统的后台数据库中。

常见的做法是，将创建数据需要用到的 SQL 语句封装成一个个的测试数据准备函数，当我们需要创建数据时，直接调用这些封装好的函数即可。

还是以用户登录功能测试为例，当我们通过 GUI 界面注册新用户时，实际上是在后端调用了 createUser 这个 API，而这个 API 的内部实现逻辑是，将用户的详细信息插入到了 userTable 和 userRoleTable 这两张业务表中。

那么此时，我们就可以直接在 userTable 和 userRoleTable 这两张业务表中插入数据，然后完成这个新用户的注册工作。

这样做的前提是，你需要知道前端用户通过 GUI 操作注册新用户时，到底修改了哪些数据库的业务表。这里，我也推荐三种方式：

直接向开发人员索要使用到的 SQL 语句；

直接阅读产品源代码；

在一个你可以独占的环境上执行 GUI 操作产生测试数据，与此同时，监控独占环境的数据库端业务表的变化，找到哪些业务表发生了变化。

通过数据库操作生成测试数据的方法，主要优点是测试数据的生成效率非常高，可以在较短的时间内创建大批量的测试数据。

当然，这个方法的缺点也非常明显，主要体现在以下几个方面：

很多时候，一个前端操作引发的数据创建，往往会修改很多张表，因此封装的数据准备函数的维护成本要高得多；

容易出现数据不完整的情况，比如一个业务操作，实际上在一张主表和一张附表中插入了记录，但是基于数据库操作的数据创建可能只在主表中插入了记录，这种错误一般都会比较隐蔽，往往只在一些特定的操作下才会发生异常；

当业务逻辑发生变化时，即 SQL 语句有变化时，需要维护和更新已经封装的数据准备函数。

综合运用 API 和数据库的方式生成测试数据

目前，在实际的工程实践中，很少使用单一的方法生成测试数据，基本都是采用 API 和数据库相结合的方式。最典型的应用场景是，先通过 API 调用生成基础的测试数据，然后使用数据库的 CRUD 操作生成符合特殊测试需求的数据。所以，你经常会看到很多的数据准备函数中，既有 API 操作，又有数据库操作。

我以创建用户为例，和你分享一下如何综合运用 API 和数据库两种方式创建测试数据吧。

假设，我们需要封装一个创建用户的函数，这个函数需要对外暴露“用户国家”和“支付方式”这两个参数。由于实际创建用户是通过后台 createUser API 完成的，但是这个 API 并不支持指定“用户国家”和“支付方式”，所以我们就需要自己封装一个创建用户的函数。

自己封装用户创建函数的方法，你可以通过下面这个思路实现：

首先，调用 createUser API 完成基本用户的创建；

然后，调用 paymentMethod API 实现用户对于不同支付方式的绑定，其中 paymentMethod API 使用的 userID 就是上一步中 createUser API 产生的用户 ID；

最后，通过数据库的 SQL 语句更新“用户国家”。

在这个例子中，createUser API 和 paymentMethod API 只是为了说明如何综合运用 API 的顺序调用，而其具体参数并不是我要阐述的关键内容，所以我并没有和你详细说明这两个 API 的参数、实现方式等问题。另外，我在最后一步综合运用了数据库的 CRUD 操作，完成了创建测试数据的全部工作。

这，就是一个封装测试数据准备函数的典型例子了。

总结

今天，我从测试数据创建的角度，和你分享了准备测试数据的四种方法。

其中，基于 GUI 操作生成测试数据是最原始的方法，但是效率很低，而且会引入不必要的依赖；通过 API 调用以及数据库操作的方式生成测试数据是目前主流的做法，通过 API 调用的方式具有数据准确度高但是创建效率较低的特点，而通过数据库的方式具有创建效率高但是维护复杂度也高的特点。

所以，在实际项目中，业界往往会综合采用 API 和数据库的方式生成测试数据，即通过 API 调用生成基础数据，然后使用数据库的 CRUD 操作进一步生成符合特殊测试需求的数据。

思考题

目前，我们需要创建的测试数据并不仅仅局限于数据库，很多时候还需要创建消息队列里面的数据。你在实际工作中遇到过这类测试数据吗？你又是如何处理的呢？

感谢你的收听，欢迎你给我留言。

36 | 浅谈测试数据的痛点

你好，我是茹炳晟。今天我和你分享的主题是：浅谈测试数据的痛点。

在上一篇文章中，我和你分享了创建测试数据的四大类方法，即基于 GUI 操作生成测试数据、通过 API 调用生成测试数据、通过数据库操作生成测试数据，以及综合运用 API 和数据库的方式生成测试数据。

但是，我并没有谈到应该在什么时机创建这些测试数据。比如，是在测试用例中实时创建测试数据，还是在准备测试环境时一下子准备好所有的测试数据呢。

其实，在不同的时机创建测试数据，就是为了解决准备测试数据的不同痛点。那么，准备测试数据的痛点，都体现在哪些方面呢？

在测试用例执行过程中，创建所需的数据往往会耗时较长，从而使得测试用例执行的时间变长；

在测试执行之前，先批量生成所有需要用到的测试数据，就有可能出现在测试用例执行时，这些事先创建好的数据已经被修改而无法正常使用了的情况；

在微服务架构下，测试环境本身的不稳定，也会阻碍测试数据的顺利创建。

那么，今天我们就先来聊聊与测试数据创建时机相关的话题。

从测试数据创建的时机来看，主要分为 On-the-fly（实时创建）和 Out-of-box（事先创建测试数据）两类方法。这两类方法都有各自的优缺点，以及适用的最佳场景。而且在工程实践中，我们往往会综合使用这两种方法。

接下来，我先和你分别介绍一下这两类方法。其实，这两类方法我已经在第 15 篇文章《过不了的坎：聊聊 GUI 自动化过程中的测试数据》中提到过了。但是，当时我只是笼统地和你分享了这两类方法的概念，并没有详细展开讨论。所以，我今天就会通过一些实例，和你更加详细地讨论这两类方法。

On-the-fly

On-the-fly 方法，又称实时创建方法，指的是在测试用例的代码中实时创建要使用到的测试数据。比如，对于用户登录功能的测试，那么在测试用例开始的部分，首先调用我在上一篇文章中介绍的创建新用户的数据准备函数来生成一个新用户，接下来的测试将会直接使用这个新创建的用户。

对于 On-the-fly，测试用例中所有用到的测试数据，都在测试用例开始前实时准备。采用 On-the-fly 方式创建的数据，都是由测试用例自己维护的，不会依赖于测试用例外的任何数据，从而保证了数据的准确性和可控性，最大程度地避免了出现“脏”数据的可能。

那到底什么是“脏”数据呢？这里的“脏”数据是指，数据在被实际使用前，已经被进行了非预期的修改。

从理论上来讲，这种由自己创建和维护数据的方式，是最佳的处理方式，很多早期的测试资料都推荐采用这种方式。但是，随着软件架构的发展，以及软件发布频率的快速增长，这种方式的弊端越来越明显，主要体现在以下三方面：

首先，实时创建测试数据比较耗时。在测试用例执行的过程中实时创建测试数据，将直接导致测试用例的整体执行时间变长。

我曾统计过一个大型电商网站的测试用例执行时间，总的测试用例执行时间中，有 30%-40% 的时间花在了测试数据的实时准备上，也就是说测试数据的实时准备花费了差不多一半的测试用例执行时间。

对传统软件企业来说，它们可能并不太在意这多出来的测试执行时间，因为它们的软件发布周期比较长，留给测试的时间也比较长，所以这多出来的时间可以忽略不计。

但是，对于互联网软件企业来说，它们的软件发布频率很高，相应地留给测试执行的时间也都很短，那么缩短测试数据的准备时间的重要性就不言而喻了。

要解决创建测试数据耗时的问题，除了从测试数据准备函数的实现入手外，还可以考虑采用我后面要介绍的事先创建测试数据 Out-of-box 的方式。

其次，测试数据本身存在复杂的关联性。很多时候你为了创建一个你需要使用的业务数据，往往需要先创建一堆其他相关联的数据，越是业务链后期的数据，这个问题就越严重。

比如，创建订单数据这个最典型的案例。由于创建订单的数据准备函数需要提供诸如卖家、买家、商品 ID 等一系列的前置数据，所以你就不得不先创建出这些前置数据。这样做，一方面测试数据准备的复杂性直线上升，另一方面创建测试数据所需要的时间也会变得更长。

为了缓解这个问题，你可以考虑将部分相对稳定的数据事先创建好，而不要采用 On-the-fly 的方式去创建所有的数据。

最后一个问题来自于微服务架构的调整。早期的软件架构都是单体的，只要测试环境部署成功了，那么所有的功能就都可以使用了。而现如今，大量的互联网产品都采用了微服务架构，所以，很多时候测试环境并不是 100% 处于全部可用的状态。也就是说，并不是所有的服务都是可用的，这就给测试数据准备带来了新的挑战。

比如，你为了测试用户登录功能，根据 On-the-fly 的策略，你首先需要创建一个新用户。假设在微服务架构下，注册用户和用户登录隶属于两个不同的微服务，而此时注册用户的微服务恰好因为某种原因处于不可用状态，那么这时你就无法成功创建这个用户，也就是无法创建测试数据。因此，整个测试用例都无法顺利执行，显然这不是我们想要的结果。

为了解决这个问题，你可以采用事先创建数据 Out-of-box 的方式，只要能够保证测试环境在某个时间段没有问题，那么就可以在这个时间段事先创建好测试数据。

为了解决上述三个问题，Out-of-box（即事先创建测试数据）的方式就应运而生了。那么，

接下来我们就一起看看这个方式的原理，以及适用的场景吧。

Out-of-box

Out-of-box 方法，又称开箱即用方法，指的是在准备测试环境时就预先将测试需要用到的数据全部准备好，而不是在测试用例中实时创建。因此，我们可以节省不少测试用例的执行时间，同时也不会存在由于环境问题无法创建测试数据而阻碍测试用例执行的情况。也就是说 Out-of-box 方法可以克服 On-the-fly 方法的缺点，那么这个方式又会引入哪些致命的新问题呢？

Out-of-box 最致命的问题是“脏”数据。

比如，我们在测试用例中使用事先创建好的用户进行登录测试，但这个用户的密码被其他人无意中修改了，导致测试用例执行时登录失败，也就不能顺利完成测试了。那么，此时这个测试用户数据就成为了“脏”数据。

再比如，我们在测试用例中使用事先创建的测试优惠券去完成订单操作，但是由于某种原因这张优惠券已经被使用过了，导致订单操作的失败，也就意味着测试用例执行失败。那么，此时这个测试优惠券数据也是“脏”数据。

由此可见，这些事先创建好的测试数据，在测试用例执行的那个时刻，是否依然可用其实是不一定的，因为这些数据很有可能在被使用前已经发生了非预期的修改。

这些非预期的修改主要来自于以下三个方面：

其他测试用例使用了这些事先创建好的测试数据，并修改了这些数据的状态；

执行手工测试时，因为直接使用了事先创建好的数据，很有可能就会修改了某些测试数据；

自动化测试用例的调试过程，修改了事先创建的测试数据；

为了解决这些“脏”数据，我们只能通过优化流程去控制数据的使用。目前，业内有些公司会将所有事先创建好的测试数据列在一个 Wiki 页面，然后按照不同的测试数据区段来分配使用对象。

比如，假设我们事先创建了 1000 个测试用户，那么用户 ID 在 0001-0200 范围内数据给这个团队使用，而用户 ID 在 0201-0500 范围内的数据则给另一个团队使用。这个分配工作，要靠流程保证，那么前提就是所有人都要遵守这些流程。

但我一直认为，但凡需要靠流程保证的一定不是最靠谱的，因为你无法确保所有人都会遵守流程。也正是因为这个原因，在实际项目中我们还是会经常看到由“脏”数据引发测试用例执行失败的案例。

更糟糕的是，如果自动化测试用例直接采用硬编码的方式，去调用那些只能被一次性使用的测试数据（比如订单数据、优惠券等）的话，你会发现测试用例只能在第一次执行时通过，后面再执行都会因为测试数据的问题而失败。

所以，你还需要在测试用例级别保证测试数据只被调用一次，而这往往会涉及到跨测试用例的测试数据维护问题，往往实现起来非常麻烦。所以说，Out-of-box 方法不适用于只能一次性使用的测试数据场景。

综合运用 On-the-fly 和 Out-of-box

为了充分利用 On-the-fly 和 Out-of-box 这两种方式的各自优点，并且规避各自的缺点，实际的工程实践中，往往是采用综合运用 On-the-fly 和 Out-of-box 的方式来实现测试数据的准备的。

在实际的测试项目中，我们可以根据测试数据的特性，把它们分为两大类，用业内的行话来讲就是“死水数据”和“活水数据”。

“死水数据”是指那些相对稳定，不会在使用过程中改变状态，并且可以被多次使用的数据。比如，商品分类、商品品牌、场馆信息等。这类数据就非常适合采用 Out-of-box 方式来创建。

这里需要特别说明的是，哪些数据属于“死水数据”并不是绝对的，由测试目的决定。

比如，用户数据在大多数的非用户相关的测试用例中基本属于“死水数据”，因为绝大多数的业务测试都会包含用户登录的操作，而且并不会去修改用户本身的数据属性，所以这时我们就可以将用户数据按照“死水数据”处理，也就是采用 Out-of-box 的方式创建。

但是，对于那些专门测试用户账号的测试用例来讲，往往会涉及到用户撤销、激活、修改密码等操作，那么此时的用户数据就不再是“死水数据”了，而应该按照“活水数据”处理。

“活水数据”是指那些只能被一次性使用，或者经常会被修改的测试数据。最典型的数据是优惠券、商品本身、订单等类似的数据。这类数据通常在被一次性使用后状态就发生了变化，不能反复使用。那么这类测试数据，就更适合采用 On-the-fly 自维护的方式。

同时，由于有 Out-of-box 数据的支持，这类数据往往不需要从最源头开始创建，而是可以基于已有的 Out-of-box 数据生成。

比如，在使用 On-the-fly 方式创建订单数据时，你可以直接使用 Out-of-box 的用户数据来作为买家数据。

由此可见，综合运用这两类方法，可以以互补的方式解决测试数据准备的很多痛点，比如测试数据准备比较耗时、测试数据存在“脏”数据的可能，以及测试环境不稳定造成的测试数据无法创建等问题。

总结

今天我从测试数据创建时机的角度，和你分享了 On-the-fly 和 Out-of-box 这两类创建数据的方式。

On-the-fly 方法又称为实时创建方法，指的是在测试用例的代码中实时创建测试用例所要使用到的测试数据，具有数据可靠性高的优点，但是会比较耗时。

而 Out-of-box 方法又称为开箱即用方法，指的是在准备测试环境时就事先准备好测试需要用到的全部数据。这样可以有效缩短测试用例的执行时间，但是存在“脏”数据的问题。

最后，我从“死水数据”和“活水数据”的角度讨论了如何综合运用上述两种方式创建测试数据，其中“死水数据”适合用 Out-of-box 的方式，而“活水数据”适合采用 On-the-fly 的方式。

思考题

你所在的项目中，采用的是什么样的测试数据准备策略，这个策略的优缺点是什么？为什么会选择这样的策略呢？另外，你所在团队会使用线上真实的数据进行测试吗？

感谢你的收听，欢迎你给我留言。

37 | 测试数据的“银弹”- 统一测试数据平台（上）

你好，我是茹炳晟。今天我和你分享的主题是：测试数据的“银弹”之统一测试数据平台（上）。

在《如何准备测试数据？》和《浅谈测试数据的痛点》这两篇文章中，我介绍了创建测试数据的主要方法，以及创建测试数据的时机。在此基础上，今天我将和你聊聊全球大型电商企业中关于准备测试数据的最佳实践。

这个主题，我会从全球大型电商企业早期的测试数据准备实践谈起，和你一起分析这些测试数据准备方法在落地时遇到的问题，以及如何在实践中解决这些问题。其实，这种分析问题、解决问题的思路，也是推动着测试数据准备时代从 1.0 到 2.0 再到 3.0 演进的原因。

所以，在这个过程中，你可以跟着时代的演进，理解测试数据准备技术与架构的发展历程，并进一步掌握 3.0 时代出现的业内处于领先地位的“统一测试数据平台”的设计思路。

因为这个主题的内容相对较多，为了降低你的学习负担、便于理解消化，我把它分成了两篇文章。同时，为了和你深入地讨论这个话题，也可以真正做到“接地气儿”，我会在这两篇文章中列举很多工程中的实际问题，并给出相应的解决方案。或许这些问题你也曾经遇到过，或者正在被其折磨，希望我给出的这些方案，可以给你启发，帮你攻克这些难关。

我们就先从数据准备的 1.0 时代谈起吧。

测试数据准备的 1.0 时代

其实，据我观察，目前很多软件企业还都处于测试数据准备的 1.0 时代。

这个阶段最典型的方法就是，将测试数据准备的相关操作封装成数据准备函数。这些相关操作，既可以是基于 API 的，也可以是基于数据库的，当然也可以两者相结合。

有了这些数据准备函数后，你就可以在测试用例内部以 On-the-fly 的方式调用它们实时创建数据，也可以在测试开始之前，在准备测试环境的阶段以 Out-of-box 的方式调用它们事先创建好测试数据。

那么，一个典型的数据准备函数长什么样子呢？我们一起来看看这段代码吧，里面的 createUser 函数，就是一个典型的数据准备函数了。

public static User createUser(String userName, String password, UserType userType, PaymentDetail paymentDetail, Country country, boolean enable2FA)

{

 // 使用 API 调用的方式和数据库 CRUD 的方式实际创建测试数据

 ...

}

乍一看，你可能觉得，如果可以将大多数的业务数据创建都封装成这样的数据准备函数，那么测试数据的准备过程就变成了调用这些函数，而无需关心数据生成的细节，这岂不是很简单、直观嘛。

但，真的是这样吗？

这里，我建议你在继续阅读后面的内容之前，先思考一下这个方法会有什么短板，然后再回过头来看答案，这将有助于加深你对这个问题的理解。当然，如果你已经在项目中实际采用了这个方法的话，相信你已经对它的短板了如指掌了。

好了，现在我来回答这个问题。利用这种数据准备函数创建测试数据方法的最大短板，在于其参数非常多、也非常复杂。在上面这段代码中，createUser 函数的参数有 6 个。而实际项目中，由于测试数据本身的复杂性、灵活性，参数的数量往往会更多，十多个都是很常见的。

而在调用数据准备函数之前，你首先要做的就是准备好这些参数。如果这些参数的数据类型是基本类型的话，还比较简单（比如，createUser 函数中 userName、password 是字符串型，enable2FA 是布尔型），但这些参数如果是对象（比如，createUser 函数的 userType、paymentDetail 和 Country 就是对象类型的参数）的话，就很麻烦了。为什么呢？

因为，你需要先创建这些对象。更糟糕的是，如果这些对象的初始化参数也是对象的话，就牵连出了一连串的数据创建操作。

下面这段代码，就是使用 createUser 函数创建测试数据的一个典型代码片段。

// 准备 createUser 的参数

UserType userType = new UserType("buyer");

Country country = new Country("US");

// 准备 createPaymentDetail 的参数

PaymentType paymentType = new PaymentType("Paypal");

// 调用 createPaymentDetail 创建 paymentDetail 对象

PaymentDetail paymentDetail = createPaymentDetail(paymentType，2000);

// 对主要的部分，调用 createUser 产生用户数据

User user=createUser(“TestUser001”, “abcdefg1234”, userType, paymentDetail, country, true)；

由此可见，每次使用数据准备函数创建数据时，你都要知道待创建数据的全部参数细节，而且还要为此创建这些参数的对象，这就让原本看似简单的、通过数据准备函数调用生成测试数据的过程变得非常复杂。

那么，你可能会问，这个过程是必须的吗，可以用个某些技术手段“跳过”这个步骤吗？

其实，绝大多数的测试数据准备场景是，你仅仅需要一个所有参数都使用了缺省值的测试数据，或者只对个别几个参数有明确的要求，而其他参数都可以是缺省值的测试数据。

以用户数据创建为例，大多情况下你只是需要一个具有缺省（Default）参数的用户，或者是对个别参数有要求的用户。比如，你需要一个美国的用户，或者需要一个 userType 是 buyer 的用户。这时，让你去人为指定所有你并不关心的参数的做法，其实是不合理的，也没有必要。

为了解决这个问题，在工程实践中，就引入了如图 1 所示的封装数据准备函数的形式。

图 1 数据准备函数的封装

在这个封装中，我们将实际完成数据创建的函数命名为 createUserImpl，这个函数内部将通过 API 调用和数据库 CRUD 操作的方式，完成实际数据的创建工作，同时对外暴露了所有可能用到的 user 参数 A、B、C、D、E。

接着，我们封装了一个不带任何参数的 createDefaultUser 函数。函数内部的实现，首先会用默认值初始化 user 的参数 A、B、C、D、E，然后再将这些参数作为调用 createUserImpl 函数时的参数。

那么，当测试用例中仅仅需要一个没有特定要求的默认用户时，你就可以直接调用这个 createDefaultUser 函数，隐藏测试用例并不关心的其他参数的细节，此时也就真正做到了用一行代码生成你想要的测试数据。

而对于那些测试用例只对个别参数有要求的场景，比如只对参数 A 有要求的场景，我们就可以为此封装一个 createXXXUser(A) 函数，用默认值初始化参数 B、C、D、E，然后对外暴露参数 A。

当测试用例需要创建 A 为特定值的用户时，你就可以直接调用 createXXXUser(A) 函数，然后 createXXXUser(A) 函数会用默认的 B、C、D、E 参数的值加上 A 的值调用 createUserImpl 函数，以此完成测试数据的创建工作。

当然，如果是对多个参数有特定要求的场景，我们就可以封装出 createYYYUser 这样暴露多个参数的函数。

通过这样的封装，对于一些常用的测试数据组合，我们通过一次函数调用就可以生成需要的测试数据；而对于那些比较偏门或者不常用的测试数据，我们依然可以通过直接调用最底层的 createUserImpl 函数完成数据创建工作。可见，这个方法相比之前已经有了很大的进步。

但是，在实际项目中，大量采用了这种封装的数据准备函数后，还有一些问题亟待解决，主要表现在以下几个方面：

对于参数比较多的情况，会面临需要封装的函数数量很多的尴尬。而且参数越多，组合也就越多，封装函数的数量也就越多。

当底层 Impl 函数的参数发生变化时，需要修改所有的封装函数。

数据准备函数的 JAR 包版本升级比较频繁。由于这些封装的数据准备函数，往往是以 JAR 包的方式提供给各个模块的测试用例使用的，并且 JAR 会有对应的版本控制，所以一旦封装的数据准备函数发生了变化，我们就要升级对应 JAR 包的版本号。
而这些封装的数据准备函数，由于需要支持新的功能，并修复现有的问题，所以会经常发生变化，因此测试用例中引用的版本也需要经常更新。

为了可以进一步解决这三个问题，同时又可以最大程度地简化测试数据准备工作，我们就迎来了数据准备函数的一次大变革，由此也将测试数据准备推向了 2.0 时代。

这里需要强调一下，我往往把到目前为止所采用的测试数据实践称为数据准备的 1.0 时代。我会在下一篇文章中，和你详细介绍 2.0 时代下的测试数据准备都有哪些关键的技术创新，相信一定会让你有眼前一亮的感觉。

总结

在 1.0 时代，准备测试数据最典型的方法就是，将测试数据准备的相关操作封装成数据准备函数。

归纳起来，这个时代的数据准备函数，主要有两种封装形式：

第一种是，直接使用暴露全部参数的数据准备函数，虽说灵活性最好，但是每次调用前都需要准备大量的参数，从使用者的角度来看便利性比较差；

第二种是，为了解决便利性差的问题，我们引入了更多的专用封装函数，在灵活性上有了很大的进步，但是也带来了可维护差的问题。

所以，为了可以更高效地准备测试数据，我们即将迎来测试数据准备的 2.0 时代，拭目以待吧。

思考题

你所在的团队，是否已经在使用我今天聊到的这些方法了呢，使用过程中还遇到了哪些挑战？如果没有使用这些方法的话，你又是采用什么方法创建测试数据的呢？

感谢你的收听，欢迎你给我留言。

38 | 测试数据的“银弹”- 统一测试数据平台（下）

你好，我是茹炳晟，今天我分享的主题是：“测试数据的“银弹”之统一测试数据平台（下）”。

在上一篇文章中，我和你分享了测试数据准备 1.0 时代的实践，在这个 1.0 时代，测试数据准备的最典型方法是，将测试数据准备的相关操作封装成数据准备函数。今天，我将继续为你介绍测试数据准备的 2.0 和 3.0 时代的实践，看看创建测试数据的方法，又发生了哪些变革。

在 1.0 时代，为了让数据准备函数使用更方便，避免每次调用前都必须准备所有参数的问题，我和你分享了很多使用封装函数隐藏默认参数初始化细节的方法。

但是，这种封装函数的方式，也会带来诸如需要封装的函数数量较多、频繁变更的维护成本较高，以及数据准备函数 JAR 版本升级的尴尬。所以，为了系统性地解决这些可维护性的问题，我们对数据准备函数的封装方式做了一次大变革，也由此进入了测试数据准备的 2.0 时代。

测试数据准备的 2.0 时代

在测试数据准备的 2.0 时代，数据准备函数不再以暴露参数的方式进行封装了，而是引入了一种叫作 Builder Pattern（生成器模式）的封装方式。这个方式能够在保证最大限度的数据灵活性的同时，提供使用上的最大便利性，并且维护成本还非常低。

事实上，如果不考虑跨平台的能力，Builder Pattern 可以说是一个接近完美的解决方案了。关于什么是“跨平台的能力”，我会在测试数据准备的 3.0 时代中解释，这里先和你介绍我们的主角：Builder Pattern。

Builder Pattern 是一种数据准备函数的封装方式。在这种方式下，当你需要准备测试数据时，不管情况多么复杂，你一定可以通过简单的一行代码调用来完成。听起来有点玄乎？没关系，看完我列举的这些实例，你马上就可以理解了。

实例一：你需要准备一个用户数据，而且对具体的参数没有任何要求。也就是说，你需要的仅仅是一个所有参数都可以采用默认值的用户。那么，在 Builder Pattern 的支持下，你只需要执行一行代码就可以创建出你需要的这个所有参数都是默认值的用户了。这行代码就是：

UserBuilder.build();

实例二：你现在还需要一个用户，但是这次需要的是一个美国的用户。那么这时，在 Builder Pattern 的支持下，你只用一行代码也可以创建出这个指定国家是美国，而其他参数都是默认值的用户。这行代码就是：

UserBuilder.withCountry("US").build();

实例三：你又需要这样一个用户数据：英国用户，支付方式是 Paypal，其他参数都是默认值。那么这时，在 Builder Pattern 的支持下，你依然可以通过一行简单的代码创建出满足这个要求的用户数据。这行代码就是：

UserBuilder.withCountry("US").withPaymentMethod("Paypal").build();

通过这三个实例，你肯定已经感受到，相对于 1.0 时代的通过封装函数隐藏默认参数初始化的方法来说，Builder Pattern 简直太便利了。

趁热打铁，我再来和你总结一下 Builder Pattern 的便利性吧：

如果仅仅需要一个全部采用缺省参数的数据的话，你可以直接使用 TestDataBuilder.build() 得到；

如果你对其中的某个或某几个参数有特定要求的话，你可以通过“.withParameter()”的方式指定，而没有指定的参数将自动采用默认值。

这样一来，无论你对测试数据有什么要求，都可以以最灵活和最简单的方式，通过一行代码得到你要的测试数据。

在实际工程项目中，随着 Builder Pattern 的大量使用，又逐渐出现了更多的新需求，为此我归纳总结了以下 4 点：

有时候，出于执行效率的考虑，我们不希望每次都重新创建测试数据，而是希望可以从被测系统的已有数据中搜索符合条件的数据；

但是，还有些时候，我们希望测试数据必须是全新创建的，比如需要验证新建用户首次登录时，系统提示修改密码的测试场景，就需要这个用户一定是被新创建的；

更多的时候，我们并不关心这些测试数据是新创建的，还是通过搜索得到的，我们只希望以尽可能短的时间得到需要的测试数据；

甚至，还有些场景，我们希望得到的测试数据一定是来自于 Out-of-box 的数据。

为了能够满足上述的测试数据需求，我们就需要在 Builder Pattern 的基础上，进一步引入 Build Strategy 的概念。顾名思义，Build Strategy 指的是数据构建的策略。

为此，我们引入了 Search Only、Create Only、Smart 和 Out-of-box 这四种数据构建的策略。这四类构建策略在 Builder Pattern 中的使用很简单，只要按照以下的代码示例指定构建策略就可以了：

UserBuilder.withCountry(“US”).withBuildStrategy(BuildStrategy.SEARCH_ONLY.build();

UserBuilder.withCountry(“US”).withBuildStrategy(BuildStrategy.CREATE_ONLY).build();

UserBuilder.withCountry(“US”).withBuildStrategy(BuildStrategy.SMART).build();

UserBuilder.withCountry(“US”).withBuildStrategy(BuildStrategy.OUT_OF_BOX).build();

结合着这四类构建策略的代码，我再和你分享一下，它们会在创建测试数据时执行什么操作，返回什么样的结果：

当使用 BuildStrategy.SEARCH_ONLY 策略时，Builder Pattern 会在被测系统中搜索符合条件的测试数据，如果找到就返回，否则就失败（这里，失败意味着没能返回需要的测试数据）；

当使用 BuildStrategy.CREATE_ONLY 策略时，Builder Pattern 会在被测系统中创建符合要求的测试数据，然后返回；

当使用 BuildStrategy.SMART 策略时，Builder Pattern 会先在被测系统中搜索符合条件的测试数据，如果找到就返回，如果没找到就创建符合要求的测试数据，然后返回；

当使用 BuildStrategy.OUT_OF_BOX 策略时，Builder Pattern 会返回 Out-of-box 中符合要求的数据，如果在 Out-of-box 中没有符合要求的数据，build 函数就会返回失败；

由此可见，引入 Build Strategy 之后，Builder Pattern 的适用范围更广了，几乎可以满足所有的测试数据准备的要求。

但是，不知道你注意到没有，我们其实还有一个问题没有解决，那就是：这里的 Builder Pattern 是基于 Java 代码实现的，如果你的测试用例不是基于 Java 代码实现的，那要怎么使用这些 Builder Pattern 呢？

在很多大型公司，测试框架远不止一套，不同的测试框架也是基于不同语言开发的，比如有些是基于 Java 的，有些是基于 Python 的，还有些基于 JavaScript 的。而非 Java 语言的测试框架，想要使用基于 Java 语言的 Builder Pattern 的话，往往需要进行一些额外的工作，比如调用一些专用函数等。

我来举个例子吧。对于 JavaScript 来说，如果要使用 Java 的原生类型或者引用的话，你需要使用 Java.type() 函数；而如果要使用 Java 的包和类的话，你就需要使用专用的 importPackage() 函数 和 importClass() 函数。

这些都会使得调用 Java 方法很不方便，其他语言在使用基于 Java 的 Builder Pattern 时也有同样的问题。

但是，我们不希望、也不可能为每套基于不同开发语言的测试框架都封装一套 Builder Pattern。所以，我们就希望一套 Builder Pattern 可以适用于所有的测试框架，这也就是我在前面提到的测试准备函数的“跨平台的能力”了。

为了解决这个问题，测试数据准备走向了 3.0 时代。

测试数据准备的 3.0 时代

为了解决 2.0 时代跨平台使用数据准备函数的问题，我们将基于 Java 开发的数据准备函数用 Spring Boot 包装成了 Restful API，并且结合 Swagger 给这些 Restful API 提供了 GUI 界面和文档。

这样一来，我们就可以通过 Restful API 调用数据准备函数了，而且由于 Restful API 是通用接口，所以只要测试框架能够发起 http 调用，就能使用这些 Restful API。于是，几乎所有的测试框架都可以直接使用这些 Restful API 准备测试数据。

由此，测试数据准备工作自然而然地就发展到了平台化阶段。我们把这种统一提供各类测试数据的 Restful API 服务，称为“统一测试数据平台”。

最初，统一测试数据平台就是服务化了数据准备函数的功能，并且提供了 GUI 界面以方便用户使用，除此以外，并没有提供其他额外功能。如图 1 所示就是统一测试数据平台的 UI 界面。

图 1 最初的统一测试数据平台 UI 界面

后来，随着统一测试数据平台的广泛使用，我们逐渐加入了更多的创新设计，统一测试数据平台的架构也逐渐演变成了如图 2 所示的样子。

图 2 演变后的统一测试数据平台架构

接下来，我和你分享一下统一测试数据平台的架构设计中最重要的两个部分：

引入了 Core Service 和一个内部数据库。其中，内部数据库用于存放创建的测试数据的元数据；Core Service 在内部数据库的支持下，提供数据质量和数量的管理机制。

当一个测试数据被创建成功后，为了使得下次再要创建同类型的测试数据时可以更高效，Core Service 会自动在后台创建一个 Jenkins Job。这个 Jenkins Job 会再自动创建 100 条同类型的数据，并将创建成功的数据的 ID 保存到内部数据库，当下次再请求创建同类型数据时，这个统一测试数据平台就可以直接从内部数据库返回已经事先创建的数据。
在一定程度上，这就相当于将原本的 On-the-fly 转变成了 Out-of-box，缩短整个测试用例的执行时间。当这个内部数据库中存放的 100 条数据被逐渐被使用，导致总量低于 20 条时，对应的 Jenkins Job 会自动把该类型的数据补足到 100 条。而这些操作对外都是透明的，完全不需要我们进行额外的操作。

这就是测试数据准备的 3.0 时代的最佳实践了。关于这个统一测试数据平台，如果你还想了解更多的技术细节，欢迎你给我留言，我们一起讨论。

总结

我和你分享了测试数据准备 2.0 时代的 Builder Pattern 实践，以及 3.0 时代的统一测试数据平台。

2.0 时代的 Builder Pattern 在提供了最大限度的数据灵活性的同时，还保证了使用上的最大便利性，并且维护成本还非常低。如果不考虑跨平台能力的话，Builder Pattern 已经是一个接近完美的解决方案了。

3.0 时代统一测试数据平台，其实是将所有的数据准备函数在 Spring Boot 的支持下转变为了 Restful API，为跨平台和跨语言的各类测试框架提供了统一的数据准备方案。

思考题

关于统一测试数据平台，由于引入了 Core Service 和内部数据库，所以可以在此基础上实现更多的高级功能。对此，你觉得还可以引入哪些功能呢？

感谢你的收听，欢迎你给我留言。

39 | 从小作坊到工厂：什么是Selenium Grid？如何搭建Selenium Grid？

你好，我是茹炳晟，今天我分享的主题是“从小作坊到工厂：什么是 Selenium Grid？如何搭建 Selenium Grid？”。

从今天开始，我们就要一起进入测试基础架构这个新的系列了。我将用四篇文章的篇幅，从 0 到 1，为你深入剖析大型互联网企业的测试基础架构设计，以及其原始驱动力，和你探讨测试执行环境设计、测试报告平台设计以及测试基础架构与 CI/CD 的集成等内容。当然，在这其中还会涉及到很多具有前瞻性的设计创新。

虽说测试基础架构是资深测试开发人员的必备技能，但此时你可能还并不清楚测试基础架构到底指的是什么？没关系，当你阅读完这个系列的文章之后，相信你一定可以对测试基础架构，以及其关键设计有一个清晰、全面的认识。

所以，今天我就先和你分享一下，我眼中的测试基础架构到底是指什么？

什么是测试基础架构？

测试基础架构指的是，执行测试的过程中用到的所有基础硬件设施以及相关的软件设施。因此，我们也把测试基础架构称之为广义的测试执行环境。通常来讲，测试基础架构主要包括以下内容：

执行测试的机器；

测试用例代码仓库；

发起测试执行的 Jenkins Job；

统一的测试执行平台；

测试用例执行过程中依赖的测试服务，比如提供测试数据的统一测试数据平台、提供测试全局配置的配置服务、生成测试报告的服务等；

…

由于测试基础架构的核心是围绕测试执行展开的，所以我们今天就先来重点讨论一下“执行测试的机器”部分。

这部分内容的展开，我会从早期最简单的方法谈起，然后探讨这个方法在实际执行中的弊端，并由此引出我们今天讨论的主角：Selenium Grid。

先试想一下：你要在一个典型测试场景中，基于某种浏览器去执行 Web 应用的 GUI 测试。这时，你首先要做的就是找到相应的机器，并确保上面已经安装了所需的浏览器。如果这台机器上，还没有安装所需浏览器的话，你需要先安装这个浏览器。一切准备就绪后，你就可以使用这台机器执行测试了。

如果你要执行的测试只需要覆盖一种浏览器的话，那就很简单了，你只要事先准备好一批专门的机器或者虚拟机，然后安装好所需的浏览器就可以了。同时，如果测试用例的数量也不是很多的话，你需要的这批机器或者虚拟机的数量也不会很多。执行测试时，你只要将需要使用的那台机器的地址提供给测试用例就可以了。

其实，这种模式就是典型的“小作坊”模式。“小作坊”模式的特点就是，人工维护一批数量不多（通常在 30 台以内）的执行测试的机器，然后按需使用。

对于小团队来讲，“小作坊”模式的问题并不大。但是，随着测试覆盖率要求的提升，以及测试用例数量的增加，这种“小作坊”模式的弊端就逐渐显现，并被不断放大了。其中，最主要问题体现在以下四个方面：

当 Web 应用需要进行不同浏览器的兼容性测试时，首先你需要准备很多台机器或者虚拟机，并安装所需的不同浏览器；然后，你要为这些机器建立一个列表，用于记录各台机器安装了什么浏览器；最后，你在执行测试时，需要先查看机器列表以选择合适的测试执行机。

当 Web 应用需要进行同一浏览器的不同版本的兼容性测试时，你同样需要准备很多安装有同一浏览器的不同版本的测试执行机，并为这些机器建立列表，记录各台机器安装的浏览器版本号，然后执行测试时先查看列表以选择合适的测试执行机。

测试执行机的机器名或者 IP 发生变化，以及需要新增或者减少测试机时，都需要人工维护这些机器列表。很显然，这种维护方式效率低下，且容易出错。

在 GUI 自动化测试用例的数量比较多的情况下，你不希望只用一台测试执行机以串行的方式执行测试用例，而是希望可以用上所有可用的测试执行机，以并发的方式执行测试用例，以加快测试速度。为了达到这个目的，你还是需要人工管理这些测试用例和测试执行机的对应关系。

这四种情况的问题，可以归结为：测试执行机与测试用例的关系是不透明的，即每个测试用例都需要人为设置测试执行机。

为了改善这种局面，Selenium Grid 就应运而生了。

一方面，使用 Selenium Grid 可以让测试机器的选择变得“透明”。也就是说，我们只要在执行测试用例时指定需要的浏览器版本即可，而无需关心如何找到合适的测试执行机。因为，这寻找符合要求的测试执行机的工作，Selenium Grid 可以帮你完成。

另一方面，Selenium Grid 的架构特点，天生就能很好地支持测试用例的并发执行。

接下来，我就和你详细聊聊到底什么是 Selenium Grid，Selenium Grid 的架构是什么样的。

图 1 Selenium Grid 的架构

从本质上讲，Selenium Grid 是一种可以并发执行 GUI 测试用例的测试执行机的集群环境，采用的是 HUB 和 Node 模式。这个概念有些晦涩难懂，我来举个例子吧。

假如，现在有个律师事务所要接受外来业务，那么就会有一个老大专门负责对外接受任务。收到任务后，这个老大会根据任务的具体要求找到合适的手下，然后将该任务分发给手下去执行。

那么，这个老大是怎么知道哪个手下最适合处理这个任务呢？其实，这个老大手下的每个人都会事先报备自己具备的技能，这样老大在分发任务的时候，就可以做到“有的放矢”了。

现在，我们再回到 Selenium Grid。Selenium Grid 由两部分构成，一部分是 Selenium Hub，另一部分是 Selenium Node。

将这个律师事务所的例子，与 Selenium Grid 做个类比，它们的对应关系是：

这个对外的老大对应的是 Selenium Hub；

具体执行任务的手下，对应的是 Selenium Node；

老大接到任务后分配给手下执行的过程，就是 Selenium Hub 将测试分配到 Selenium Node 执行的过程；

老大的手下向他报备自己技能的过程，就是 Selenium Node 向 Selenium Hub 注册的过程。

也就是说，Selenium Hub 用来管理各个 Selenium Node 的注册信息和状态信息，并且接收远程客户端代码的测试调用请求，并把请求命令转发给符合要求的 Selenium Node 执行。

现在，我们已经搞明白了什么是 Selenium Grid，以及 Selenium Grid 的工作模式。Selenium Grid 的功能是不是很酷炫呢？那么，Selenium Grid 的搭建是不是很难？接下来，我们就看看如何搭建自己的 Selenium Grid 吧。

在这里，我会依次给你介绍传统的 Selenium Grid 和基于 Docker 的 Selenium Grid 的搭建方法。通过这部分内容我要达到的目的是，可以帮你搭建起属于自己的 Selenium Grid。

传统 Selenium Grid 的搭建方法

我将通过一个实例，和你分享如何搭建一个传统的 Selenium Grid。

现在，我们的需求是，搭建一个具有 1 个 Node 的 Selenium Grid。那么通常来讲我们需要 2 台机器，其中一台作为 Hub，另外一台作为 Node，并要求这两台机器已经具备了 Java 执行环境。

通过官网下载 selenium-server-standalone-.jar 文件。这里需要注意的是，不管是 Hub 还是 Node，都使用同一个 JAR 包启动，只是启动参数不同而已。

将下载的 selenium-server-standalone-.jar 文件分别复制到两台机器上。

选定其中一台机器作为 Selenium Hub，并在这台机器的命令行中执行以下命令：

java -jar selenium-server-standalone-<version>.jar -role hub

在这条命令中，“-role hub”的作用是将该机器启动为 Selenium Hub。启动完成后，这台机器默认对外提供服务的端口是 4444。

然后，你就可以在这台机器上通过http://localhost:4444/grid/console观察 Selenium Hub 的状态，也可以在其他机器上通过 http://<Hub_IP>:4444/grid/console 观察 Selenium Hub 的状态。其中，<Hub_IP> 是这台 Selenium Hub 机器的 IP 地址。由于此时还没有 Node 注册到该 Hub 上，所以你看不到任何的 Node 信息。

启动过程和状态信息，分别如图 2、3 所示。

图 2 Selenium Hub 启动过程

图 3 没有挂载任何 Node 的 Selenium Hub

在另一台作为 Selenium Node 的机器上执行以下命令：

java -jar selenium-server-standalone-<version>.jar -role node -hub http:// <Hub_IP>:4444/grid/register

这条命令中，“-role node”的作用是，将该机器启动为 Selenium Node，并且通过“-hub”指定了 Selenium Hub 的节点注册 URL。

执行成功后，你可以再次打开 http://<Hub_IP>:4444/grid/console 观察 Selenium Hub 的状态。此时，你可以看到已经有一个 Node 挂载到了 Hub 上。这个 Node，就是用来实际执行测试的机器了。并且，这个 Node 上已经缺省提供了 5 个 Firefox 浏览器的实例、5 个 Chrome 浏览器的实例和 1 个 IE 浏览器的实例，同时默认允许的并发测试用例数是 5 个。

如果你想自己配置这些内容，可以在启动 Node 的时候提供不同的启动参数。具体可以指定哪些参数，你可以参考Selenium Grid的官方文档。

如图 4 所示为 Node 的启动过程，如图 5 所示为在 Hub 端注册 Node 的过程，如图 6 所示为挂载完 Node 后 Selenium Hub 的状态。

图 4 Node 的启动过程

图 5 Hub 端 Node 注册的过程

图 6 挂载完 Node 后的 Selenium Hub 状态

完成上述操作后，在测试用例中通过以下代码将测试指向 Selenium Hub，然后由 Selenium Hub 完成实际测试执行机的分配与调度工作。其中，最关键的部分是，创建 RemoteWebDriver 实例的第一个参数，这个参数不再是一个具体的测试执行机的 IP 地址或者名字了，而是 Selenium Hub 的地址。

DesiredCapabilities capability = DesiredCapabilities.firefox();

WebDriver driver = new RemoteWebDriver(new URL("http://<Hub_IP>:4444/wd/hub"), capability);

至此，我们就已经完成了 Selenium Grid 的搭建工作。正如上面的五个步骤所示，这个搭建过程非常简单。接下来，你就自己动手尝试一下吧。

基于 Docker 的 Selenium Grid 的搭建方法

目前，Docker 技术的广泛普及，再加上它的轻量级、灵活性等诸多优点，使得很多软件都出现了 Docker 版本。当然，Selenium Grid 也不例外。所以，我也会在这里和你简单介绍一下基于 Docker 的 Selenium Grid 搭建过程。

在这个搭建过程中，你将会发现基于 Docker 运行 Selenium Grid 的话，机器的利用率会得到大幅提高。因为，一台实体机或者虚拟机，往往可以运行非常多的 Docker 实例数量，而且 Docker 实例的启动速度也很快。因此，相对于虚拟机或者实体机方案而言，Docker 方案可以更高效地创建 Node。

接下来，我们就一起看看如何基于 Docker 来搭建 Selenium Grid 吧。

在基于 Docker 搭建 Selenium Grid 之前，你需要先安装 Docker 环境。具体安装方法，你可以参考Docker 的官方文档。

接下来，你就可以通过以下命令分别启动 Selenium Hub 和 Selenium Node 了。

# 创建了 Docker 的网络 grid

$ docker network create grid

# 以 Docker 容器的方式启动 Selenium Hub，并且对外暴露了 4444 端口

$ docker run -d -p 4444:4444 --net grid --name selenium-hub selenium/hub:3.14.0-europium

# 以 Docker 容器的方式启动并挂载了 Chrome 的 Selenium Node

$ docker run -d --net grid -e HUB_HOST=selenium-hub -v /dev/shm:/dev/shm selenium/node-chrome:3.14.0-europium

# 以 Docker 容器的方式启动并挂载了 Firefox 的 Selenium Node

$ docker run -d --net grid -e HUB_HOST=selenium-hub -v /dev/shm:/dev/shm selenium/node-firefox:3.14.0-europium

相比基于实体机或者虚拟机搭建 Selenium Grid 的方法，基于 Docker 的方式灵活性更大、启动效率也更高、可维护性也更好。而且，在更高级的应用中，比如当我们需要根据测试用例的排队情况，动态增加 Selenium Grid 中的 Node 数量的时候，Docker 都将是最好的选择。关于这部分内容具体的细节，我会在后面两篇文章中详细展开。

总结

今天，我从测试基础架构的概念讲起，并和你分享了传统 Selenium Grid 和基于 Docker 的 Selenium Grid 的搭建方法。

首先，测试基础架构指的是，执行测试的过程中用到的所有基础硬件设施以及相关的软件设施，包括了执行测试的机器、测试用例代码仓库、统一的测试执行平台等。而，今天我针对测试执行的机器这个主题展开了分享。

在最早起的测试执行场景中，采用的方法是由人工维护一批数量不多（通常在 30 台以内）的执行测试的机器，然后按需使用，完成整个测试过程，这也是典型的“小作坊”模式。随着测试需求日益复杂，“小作坊”模式的缺点也暴露无疑，其中最主要的问题在于：测试执行机和测试用例的对应关系不“透明”，以及由此带来的测试用例并发执行难以实施的问题。

于是，为了解决这个问题，就出现了 Selenium Grid。简单地说，Selenium Grid 就是一种可以并发执行 GUI 测试用例的测试执行机的集群环境。由于它采用的是 Hub 和 Node 的架构模式，所以很容易就解决了“小作坊”模式的测试用例与测试执行机间的不“透明”关系，以及测试用例并发执行的问题。

而 Selenium Grid 的搭建也是非常简单。其中，传统 Selenium Grid 搭建时只要在理解了 Selenium Grid 架构之后，通过 Java 命令分别启动 Hub 和 Node 即可；而基于 Docker 的 Selenium Grid 在搭建时，就更简单了，直接通过 Docker 命令运行已经封装好的 Image 就可以了。

这么来看，Selenium Grid 功能强大，搭建方法更是简单，也因此已经广泛应用于测试执行环境的搭建中。

思考题

目前 Selenium Grid 已经有 Docker 的版本了，你有没有考虑过可以在云端，比如 PCF、GCP、AWS 上搭建 Selenium Grid 呢？在我看来，这将是未来的主流方案，你也是类似的看法吗？

感谢你的收听，欢迎你给我留言。

40 | 从小工到专家：聊聊测试执行环境的架构设计（上）

你好，我是茹炳晟，今天我和你分享的主题是“从小工到专家：聊聊测试执行环境的架构设计（上）”。

在上一篇文章中，我介绍了 Selenium Grid 的基础知识，以及如何搭建 Selenium Grid。现在，你已经非常清楚，Selenium Grid 的作用主要是承担了测试执行机器的角色，被用来执行实际的测试工作。但是，实际工程中的测试执行环境往往更复杂，而测试执行机器也只是其中的一个重要部分。

因此，我们还需要控制发起测试的 Jenkins，并管理测试用例执行和结果显示的系统。同时，为了更方便地与 CI/CD 流水线集成，我们还希望不同类型的测试发起过程可以有统一的接口。

那么，从今天开始的两篇文章，我将由浅入深地和你聊聊测试执行环境中的基本概念，以及架构设计的思路。

什么是测试执行环境？

测试执行环境的定义有广义和狭义之分：

狭义的测试执行环境，单单指测试执行的机器或者集群。比如，我在上一篇文章《从小作坊到工厂：什么是 Selenium Grid？如何搭建 Selenium Grid？》中介绍的 Selenium Grid 就是一个最经典的测试执行集群环境。

广义的测试执行环境，除了包含具体执行测试的测试执行机以外，还包括测试执行的机器或者集群的创建与维护、测试执行集群的容量规划、测试发起的控制、测试用例的组织以及测试用例的版本控制等等。

因此，广义的测试执行环境也被称为测试基础架构。而，我在测试基础架构这个系列里，要和你讨论的是广义上的测试执行环境。也就是说，我会和你重点讨论测试基础架构的概念和设计。

如果你是在一些小型的软件公司做测试工程师的话，可能并没有听说过“测试基础架构”这个概念，或者也只是停留在对其的一知半解上。但，实际情况是，无论小型的软件公司还是中大型的软件公司都存在测试基础架构。

只是，在小型的软件公司，由于自动化测试的执行量相对较小，测试形式也相对单一，所以测试执行架构非常简单，可能只需要几台固定的专门用于测试执行的机器就可以了。那么，此时测试基础架构的表现形式就是测试执行环境。

而对于中大型的软件公司，尤其是大型的全球化电商企业，由于需要执行的自动化测试用例数量非常多，再加上测试本身的多样性需求，测试基础架构的设计是否高效和稳定将直接影响产品是否可以快速迭代、发布上线。因此，中大型的软件公司都会在测试基础架构上有比较大的投入。

一般情况下，中大型企业在测试基础架构上的投入，主要是为了解决以下这几方面的问题：

简化测试的执行过程。我们不用每次执行测试时，都必须先去准备测试执行机，因为测试执行机的获取就像日常获取水电一样方便了。

最大化测试执行机器的资源利用率，使得大量的测试执行机可以以服务的形式为公司层面的各个项目团队提供测试执行的能力。

提供大量测试用例的并发执行能力，使得我们可以在有限的时间内执行更多的测试用例。

提供测试用例的版本控制机制，使得测试执行的时候可以根据实际被测系统的软件版本自动选择对应的测试用例版本。

提供友好的用户界面，便于测试的统一管理、执行与结果展示。

提供了与 CI/CD 流水线的统一集成机制，从而可以很方便地在 CI/CD 流水线中发起测试调用。

以此类推，如果你想要设计出高效的测试基础架构，就必须要从以下几个方面着手：

对使用者而言，测试基础架构的“透明性”。也就是说，测试基础架构的使用者，无需知道测试基础架构的内部设计细节，只要知道如何使用就行。

我在上一篇文章中和你探讨的 Selenium Grid，就是一个很好的案例。实际使用 Selenium Grid 时，你只需要知道 Hub 的地址，以及测试用例对操作系统和浏览器的要求就可以，而无需关注 Selenium Grid 到底有哪些 Node，以及各个 Node 又是如何维护的技术细节。

对维护者而言，测试基础架构的“易维护性”。对于一些大型的测试而言，你需要维护的测试执行机的数量会相当大，比如 Selenium 的 Node 的数量达到成百上千台后，如果遇到 WebDriver 升级、浏览器升级、病毒软件升级的情况时，如何高效地管理数量庞大的测试执行机将会成为一大挑战。

所以，早期基于物理机和虚拟机时，这个执行机的管理问题就非常严重。但是，出现了基于 Docker 的方案后，这些问题都因为 Docker 容器的技术优势而被轻松解决了。

对大量测试用例的执行而言，测试基础架构执行能力的“可扩展性”。这里的可扩展性指的是，测试执行集群的规模可以随着测试用例的数量自动扩容或者收缩。

以 Selenium Gird 为例，可扩展性就是 Node 的数量和类型，可以根据测试用例的数量和类型进行自动调整。这里，建议你先记住这个概念，我还会在专栏的后续文章中详细展开。

随着移动 App 的普及，测试基础架构中的测试执行机需要支持移动终端和模拟器的测试执行。目前，很多的商业云测平台已经可以支持各种手机终端的测试执行了。其后台实现，基本都是采用的 Appium + OpenSTF + Selenium Gird 的方案。

很多中小企业，因为技术水平以及研发成本的限制，一般直接使用这类商业解决方案。但是，对于大型企业来说，出于安全性和可控制性的考量，一般会选择自己搭建移动测试执行环境。

理解了什么是测试执行环境后，我们再一起看看测试基础架构的设计吧。

但是，这里我需要说明的是，我并不会以目前业界的最佳实践为例，和你讨论应该如何设计测试基础架构。

为什么呢？因为这样做，虽然看似可以简单粗暴地解决实际问题，但是这中间涉及到的琐碎问题，将会淹没测试基础架构设计的主线，反而会让你更加困惑为什么我要这么做，而不能那么做。

因此，本着“知其所以然”的原则，我还是会以遇到问题然后解决问题的思路，由浅入深地从最早期的测试基础架构说起，带你一起去经历一次测试基础架构设计思路的演进。在我看来，这样的思路，才是深入理解一门技术的有效途径，也希望你可以借此将测试基础架构的关键问题吃得更透。

早期的测试基础架构

早期的测试基础架构，会将测试用例存储在代码仓库中，然后是用 Jenkins Job 来 Pull 代码并完成测试的发起工作。如图 1 所示。

图 1 早期的测试基础架构

在这种架构下，自动化测试用例的开发和执行流程，是按照以下步骤执行的：

自动化测试开发人员在本地机器开发和调试测试用例。这个开发和调试过程，通常是测试开发人员自己的工作电脑上进行。也就是说，他们在开发完测试用例后，会在本机执行测试用例。这些测试用例，会在本机打开指定的浏览器并访问被测网站的 URL，然后发起业务操作，完成自动化测试。

将开发的测试用例代码，Push 到代码仓库。如果自动化测试脚本在测试开发人员本地的电脑上顺利执行完成，那么接下来，我们就会将测试用例的代码 Push 到代码仓库，至此标志着自动化测试用例的开发工作已经完成。

在 Jenkins 中建立一个 Job，用于发起测试的执行。这个 Jenkins Job 的主要工作是，先从测试用例代码仓库中 Pull 测试用例代码，并发起构建操作；然后，在远端或者本地固定的测试执行机上发起测试用例的执行。
这个 Jenkins Job 通常会将一些会发生变化的参数作为 Job 自身的输入参数。比如，远端或者本地固定的测试执行机的 IP 地址或者名字；再比如，被测系统有多套环境，需要指定被测系统的具体名字等。

这种测试架构，对于测试用例数量不多、被测系统软件版本不太复杂的场景的测试需求，基本都可以满足。但在实际使用时，你总会感觉哪里不太方便。

比如，每次通过 Jenkins Job 发起测试时，你都需要填写测试用例需要在哪台测试执行机上执行。而此时，这台测试执行机是否处于可用状态，是否正在被其他测试用例占用都是不可知的，那么你就需要在测试发起前进行人为确认，或者开发一个执行机器环境检查的脚本帮你确认。并且，当远端测试执行机的 IP 或者名字有变化时，或者当远端测试执行机的数量有变动时，你都需要能提前获知这些信息。

所以，这些局限性，也就决定了这种架构只能适用于小型项目。

说到这里，你可能已经想到了，不是有 Selenium Grid 吗？我完全可以用 Selenium Gird 代替固定的测试执行机。没错，这就是测试基础架构的第一次的重大演进，也因此形成了目前已经被广泛使用的经典测试基础架构。

经典的测试基础架构

用 Selenium Grid 代替早期测试基础架构中的“远端或本地固定的测试执行机器”，就形成了经典的测试基础架构。其架构如图 2 所示。

图 2 经典的测试基础架构

这样，你在每次发起测试时，就不再需要指定具体的测试执行机器了，只要提供固定的 Selenium Hub 地址就行，然后 Selenium Hub 就会自动帮你选择合适的测试执行机。

同时，由于 Selenium Grid 中 Node 的数量可以按需添加，所以整体的测试执行任务比较重时，你就可以增加 Grid 中 Node 的数量。

另外，Selenium 还支持测试用例的并发执行，可以有效缩短整体的测试执行时间。

所以，这种基于 Selenium Grid 的经典测试基础架构，已经被大量企业广泛采用。

但是，随着测试用例数量的继续增加，传统的 Selenium Grid 方案在集群扩容、集群 Node 维护等方面遇到了瓶颈，并且 Jenkins Job 也因为测试用例的增加变得臃肿不堪。因此，变革经典的测试基础架构的呼声，也越来越高。

为此，业界考虑将 Selenium Grid 迁移到 Docker，并且提供便于 Jenkins Job 管理的统一测试执行平台。这也是我将在下一篇文章中，要和你继续讨论的话题。

拭目以待吧。

总结

从广义上讲，测试执行环境除了包括测试执行机以外，还包括测试执行机的维护、集群的容量规划、测试发起的控制、测试用例的组织以及测试用例的版本控制等等。这也就是我要和你的测试基础架构的定义。

从定义上，我们也可以看出在设计一个高效的测试基础架构，应该从这几个方面着手：

保证对使用者的“透明性”；

需要具备对维护者而言的“易维护性”；

做到对大量测试用例并发执行的“可扩展性”；

兼顾移动 App 对测试执行环境的需求。

然后，我以遇到问题然后解决问题的思路，和你分享了早期的测试基础架构向经典的测试基础架构的演进。这个演变可以归纳为，用 Selenium Grid 代替早期测试基础架构中的“远端或本地固定的测试执行机器”，打破了因为需要人为指定并维护测试执行机，而只能适用于小型测试项目的局限性，从而形成了已被广泛使用的经典测试基础架构。

而经典的测试基础架构，在测试用例持续增加时，也会面临诸如集群扩容、Jenkins Job 臃肿不堪等等问题，于是基于 Docker 的测试基础架构便应运而生了。

思考题

你所在的团队是否在使用 Selenium Grid？在实际使用过程中，你是否有遇到过什么瓶颈，又是如何解决的呢？

感谢你的收听，欢迎你给我留言一起讨论。

41 | 从小工到专家：聊聊测试执行环境的架构设计（下）

你好，我是茹炳晟，今天我和你分享的主题是“从小工到专家：聊聊测试执行环境的架构设计（下）”。

在上一篇文章中，我介绍了测试基础架构的概念，以及早期的和经典的两种测试基础架构。在文章的最后，我提到经典的测试基础架构中采用的 Selenium Grid 方案，在测试用例的数量持续增加的情况下，会带来集群扩容、Jenkins Job 臃肿不堪等诸多问题，因此我们考虑将 Selenium Grid 迁移到 Docker，并且提供便于 Jenkins Job 管理的统一测试执行平台。

所以，今天的这篇文章，我就会围绕这些瓶颈以及对应的解决方案来展开。

基于 Docker 实现的 Selenium Grid 测试基础架构

随着测试基础架构的广泛使用，以及大量的浏览器兼容性测试的需求，Selenium Grid 中 Node 的数量会变得越来越大，也就是说我们需要维护的 Selenium Node 会越来越多。

在 Node 数量只有几十台的时候，通过人工的方式去升级 WebDriver、更新杀毒软件、升级浏览器版本，可能还不是什么大问题。但是，当需要维护的 Node 数量达到几百台甚至几千台的时候，这些 Node 的维护工作量就会直线上升。虽然，你可以通过传统的运维脚本管理这些 Node，但维护的成本依然居高不下。

同时，随着测试用例数量的持续增长，Selenium Node 的数量也必然会不断增长，这时安装部署新 Node 的工作量也会难以想象。因为，每台 Node 无论是采用实体机还是虚拟机，都会牵涉到安装操作系统、浏览器、Java 环境，以及 Selenium。

而目前流行的 Docker 容器技术，由于具有更快速的交付和部署能力、更高效的资源利用，以及更简单的更新维护能力，也就使得 Docker 相比于传统虚拟机而言，更加得“轻量级”。

因此，为了降低 Selenium Node 的维护成本，我们自然而然地想到了目前主流的容器技术，也就是使用 Docker 代替原本的虚拟机方案。

基于 Docker 的 Selenium Grid，可以从三个方面降低我们维护成本：

由于 Docker 的更新维护更简单，使得我们只要维护不同浏览器的不同镜像文件即可，而无需为每台机器安装或者升级各种软件；

Docker 轻量级的特点，使得 Node 的启动和挂载所需时间大幅减少，直接由原来的分钟级降到了秒级；

Docker 高效的资源利用，使得同样的硬件资源可以支持更多的 Node。也就是说，我们可以在不额外投入硬件资源的情况下，扩大 Selenium Grid 的并发执行能力。

因此，现在很多大型互联网企业的测试执行环境都在向 Docker 过渡。

而具体如何基于 Docker 搭建一套 Selenium Grid，你可以参考我在第 39 篇文章《从小作坊到工厂：什么是 Selenium Grid？如何搭建 Selenium Grid？》中介绍的方法。由此可见，将原本基于实体机或者虚拟机实现的 Selenium Grid 改进成为基于 Docker 实现的过程也很简单、灵活。

如图 1 所示，就是一个基于 Docker 实现的 Selenium Grid 的测试基础架构。

图 1 基于 Docker 实现的 Selenium Grid 测试基础架构

引入统一测试执行平台的测试基础架构

在实际的使用过程中，基于 Docker 的 Selenium Grid 使得测试基础架构的并发测试能力不断增强，也因此会有大量项目的大量测试用例会运行在这样的测试基础架构之上。

当项目数量不多，我们可以直接通过手工配置 Jenkins Job，并直接使用这些 Job 控制测试的发起和执行。但是，当项目数量非常多之后，测试用例的数量也会非常多，这时新的问题又来了：

管理和配置这些 Jenkins Job 的工作量会被不断放大；

这些 Jenkins Job 的命名规范、配置规范等也很难实现统一管理，从而导致 Jenkins 中出现了大量重复和不规范的 Job；

当需要发起测试，或者新建某些测试用例时，都要直接操作 Jenkins Job。而这个过程，对于不了解这些 Jenkins Job 细节的人（比如，新员工、项目经理、产品经理）来说，这种偏技术型的界面体验就相当不友好了。

为此，我们为了管理和执行这些发起测试的 Jenkins Job 实现了一个 GUI 界面系统。在这个系统中，我们可以基于通俗易懂的界面操作，完成 Jenkins Job 的创建、修改和调用，并且可以管理 Jenkins Job 的执行日志以及测试报告。

这，其实就是统一测试执行平台的雏形了。

有了这个测试执行平台的雏形后，我们逐渐发现可以在这个平台上做更多的功能扩展，于是这个平台就逐渐演变成了测试执行的统一入口。

在这里，我列举了这个平台两个最主要的功能和创新设计，希望可以给你以及你所在公司的测试基础架构建设带来一些启发性的思考。

第一，测试用例的版本化管理。我们都知道，应用的开发有版本控制机制，即：每次提测、发布都有对应的版本号。所以，为了使测试用例同样可追溯，也就是希望不同版本的开发代码都能有与之对应的测试用例，很多大型企业或者大型项目都会引入测试用例的版本化管理。最简单直接的做法就是，采用和开发一致的版本号。

比如，被测应用的版本是 1.0.1，那么测试用例的版本也命名为 1.0.1。在这种情况下，当被测应用版本升级到 1.0.2 的时候，我们会直接生成一个 1.0.2 版本的测试用例，而不应该直接修改 1.0.1 版本的测试用例。

这样，当被测环境部署的应用版本是 1.0.1 的时候，我们就选择 1.0.1 版本的测试用例；而当被测环境部署的应用版本是 1.0.2 的时候，我们就相应地选择 1.0.2 版本的测试用例。

所以，我们就在这个统一的测试执行平台中，引入了这种形式的测试用例版本控制机制，直接根据被测应用的版本自动选择对应的测试用例版本。

第二，提供基于 Restful API 的测试执行接口供 CI/CD 使用。这样做的原因是，测试执行平台的用户不仅仅是测试工程师以及相关的产品经理、项目经理，很多时候 CI/CD 流水线才是主力用户。因为，在 CI/CD 流水线中，每个阶段都会有不同的发起测试执行的需求。

我们将测试基础架构与 CI/CD 流水线集成的早期实现方案是，直接在 CI/CD 流水线的脚本中硬编码发起测试的命令行。这种方式最大的缺点在于灵活性差：

当硬编码的命令行发生变化，或者引入了新的命令行参数的时候，CI/CD 流水线的脚本也要一起跟着修改；

当引入了新的测试框架时，发起测试的命令行也是全新的，那么 CI/CD 流水线的脚本也必须被一起改动。

因此，为了解决耦合性的问题，我们在这个统一的测试执行平台上，提供了基于 Restful API 的测试执行接口。任何时候你都可以通过一个标准的 Restful API 发起测试，CI/CD 流水线的脚本也无须再知道发起测试的命令行的具体细节了，只要调用统一的 Restful API 即可。

如图 2 所示，就是引入了统一测试执行平台的测试基础架构。

图 2 引入统一测试执行平台的测试基础架构

基于 Jenkins 集群的测试基础架构

这个引入了统一测试执行平台的测试基础架构，看似已经很完美了。但是，随着测试需求的继续增长，又涌现出了新的问题：单个 Jenkins 成了整个测试基础架构的瓶颈节点。因为，来自于统一测试执行平台的大量测试请求，会在 Jenkins 上排队等待执行，而后端真正执行测试用例的 Selenium Grid 中很多 Node 处于空闲状态。

为此，将测试基础架构中的单个 Jenkins 扩展为 Jenkins 集群的方案就势在必行了。如图 3 所示，就是基于 Jenkins 集群的测试基础架构。

图 3 基于 Jenkins 集群的的测试基础架构

因为 Jenkins 集群中包含了多个可以一起工作的 Jenkins Slave，所以大量测试请求排队的现象就再也不会出现了。

而这个升级到 Jenkins 集群的过程中，对于 Jenkins 集群中 Slave 的数量到底多少才合适并没有定论。一般的做法是，根据测试高峰时段 Jenkins 中的排队数量来预估一个值。通常最开始的时候，我们会使用 4 个 Slave 节点，然后观察高峰时段的排队情况，如果还是有大量排队，就继续增加 Slave 节点。

测试负载自适应的测试基础架构

引入了 Jenkins 集群后，整个测试基础架构已经很成熟了，基本上可以满足绝大多数的测试场景了。但是，还有一个问题一直没有得到解决，那就是：Selenium Grid 中 Node 的数量到底多少才合适？

如果 Node 数量少了，那么当集中发起测试的时候，就会由于 Node 不够用而造成测试用例的排队等待，这种场景在互联网企业中很常见；

而如果 Node 数量多了，虽然可以解决测试高峰时段的性能瓶颈问题，但是又会造成空闲时段的计算资源浪费问题。当测试基础架构搭建在按使用付费的云端时，计算资源的浪费就是资金浪费了。

为了解决这种测试负载不均衡的问题，Selenium Grid 的自动扩容和收缩技术就应运而生了。

Selenium Grid 的自动扩容和收缩技术的核心思想是，通过单位时间内的测试用例数量，以及期望执行完所有测试的时间，来动态计算得到所需的 Node 类型和数量，然后再基于 Docker 容器快速添加新的 Node 到 Selenium Grid 中；而空闲时段则去监控哪些 Node 在指定时间长短内没有被使用，并动态地回收这些 Node 以释放系统资源。

通常情况下，几百乃至上千台 Node 的扩容都可以在几分钟内完成，Node 的销毁与回收的速度同样非常快。

至此，测试基础架构已经演变得很先进了，基本可以满足大型电商的测试执行需求了。测试负载自适应的测试基础架构，具体如图 4 所示。

图 4 测试负载自适应的测试基础架构

如何选择适合自己的测试基础架构？

现在，我已经介绍完了测试基础架构的演进，以及其中各阶段主要的架构设计思路，那么对于企业来说，应当如何选择最适合自己的测试基础架构呢？

其实，对于测试基础架构的建设，我们切忌不要为了追求新技术而使用新技术，而是应该根据企业目前在测试执行环境上的痛点，来有针对性地选择与定制测试基础架构。

比如，你所在的企业如果规模不是很大，测试用例执行的总数量相对较少，而且短期内也不会有大变化的情况，那么你的测试基础架构完全就可以采用经典的测试基础架构，而没必要引入 Docker 和动态扩容等技术。

再比如，如果是大型企业，测试用例数量庞大，同时还会存在发布时段大量测试请求集中到来的情况，那么此时就不得不采用 Selenium Gird 动态扩容的架构了。而一旦要使用动态扩容，那么势必你的 Node 就必须做到 Docker 容器化，否则无法完全发挥自动扩容的优势。

所以说，采用什么样的测试基础架构不是由技术本身决定的，而是由测试需求推动的。

总结

在今天这篇文章中，我从测试基础架构演进的视角，和你分享了测试基础架构发展的前世今生。

首先，为了降低测试用例过多时 Selenium Grid 的维护成本，我们用 Docker 容器代替了经典测试基础架构中的实体机 / 虚拟机，形成了基于 Docker 实现的 Selenium Grid 测试基础架构。

而后，我们发现测试用例的数量达到一定规模后，管理和执行发起测试的 Jenkins Job 成了问题。于是，我们引入了一个基于 GUI 界面的测试执行平台，并在其上扩展了诸如测试用例版本化、提供基于 Restful API 的测试执行接口等功能。从而，形成了由这个统一测试执行平台发起测试的测试基础架构形态。

而为了进一步解决由单个 Jenkins 带来的系统瓶颈问题，我们过渡到了基于 Jenkins 集群的测试基础架构，通过多个同时工作的 Jenkins Slave，解决了大量测试请求排队的问题。

随后，为了解决 Selenium Grid 中 Node 的数量到底多少才合适的问题，我和你谈论了创新设计的 Selenium Grid 的自动扩容和收缩技术。至此，测试负载自适应的测试基础架构也终于“千呼万唤始出来”了。

最后，我谈论了不同企业该如何选择最适合自己的测试基础架构的问题。这里，我强调了一定要根据测试需求选择测试基础架构，而不能一味地追求最新的技术。

我希望通过这种授人以渔的方式，可以帮你拓宽思路，并将测试基础架构的设计思路、思想，运用到你的实际工作中去。

思考题

其实，在我讲述的这个测试基础架构的设计和搭建过程中，还有很多可以优化和创新的点。你觉得哪些部分可以再优化呢？你还有哪些想法呢？

欢迎你给我留言，我们一起讨论。

42 | 实战：大型全球化电商的测试基础架构设计

你好，我是茹炳晟。今天我和你分享的主题是“实战：大型全球化电商的测试基础架构设计”。

在前面的两篇文章中，我和你分享了测试基础架构的设计以及演进之路，其中涉及到了统一测试执行平台、Selenium Grid 和 Jenkins 等一系列的概念。

在掌握了这些基础内容之后，今天我就和你一起看看大型全球化电商的测试基础架构又是如何设计的。这其中除了我之前介绍过的概念以外，还会引入一些新的服务和理念，我都会和你一一道来。

因为我们已经掌握了测试基础架构设计的基础知识，所以今天我会采用一种不同于以往由浅入深的方式，直接给出大型全球化电商网站的全局测试基础架构的最佳实践，然后再依次解释各个模块的主要功能以及实现基本原理。

其实，大型全球化电商网站全局测试基础架构的设计思路，可以总结为“测试服务化”。也就是说，测试过程中需要用的任何功能都通过服务的形式提供，每类服务完成一类特定功能，这些服务可以采用最适合自己的技术栈，独立开发，独立部署。而至于到底需要哪些测试服务，则是在理解了测试基础架构的内涵后再高度抽象后得到的。从本质上来看，这种设计思想其实和微服务不谋而合。

根据在大型全球化电商网站工作的实际经验，我把一个理想中的测试基础架构概括为了一张图（如图 1 所示）。

图 1 大型全球化电商网站的全局测试基础架构设计

这个理想的测试基础架构，包括了 6 种不同的测试服务，分别是：统一测试执行服务、统一测试数据服务、全局测试配置服务、测试报告服务、测试执行环境准备服务，以及被测系统部署服务。

接下来，我们一起看看这 6 大测试服务，具体是什么，以及如何实现。

统一测试执行服务

从本质上看，统一测试执行服务，其实和统一测试执行平台（你可以再回顾一下第 41 篇文章《从小工到专家：聊聊测试执行环境的架构设计（下）》）是一个概念。只不过，统一测试执行服务，强调的是服务，也就是强调测试执行的发起是通过 Restful API 调用完成的。

总结来说，以 Restful API 的形式对外提供测试执行服务的方式，兼具了测试版本管理、Jenkins 测试 Job 管理，以及测试执行结果管理的能力。

统一测试执行服务的主要原理是，通过 Spring Boot 框架提供 Restful API，内部实现是通过调度 Jenkins Job 具体发起测试。如果你对此还有疑惑，请参考第 40 篇文章《从小工到专家：聊聊测试执行环境的架构设计（上）》。

还记得我在前面一直提到的将测试发起与 CI/CD 流水线集成吗？这个统一测试执行服务采用的 Restful API 调用，主要用户就是 CI/CD 流水线脚本。我们可以在这些脚本中，通过统一的 Restful API 接口发起测试。

统一测试数据服务

统一测试数据服务，其实就是统一测试数据平台（你也可以再回顾一下第 37 篇《测试数据的“银弹”- 统一测试数据平台（上）》和第 38 篇《测试数据的“银弹”- 统一测试数据平台（下）》文章的内容）。

任何测试，但凡需要准备测试数据的，都可以通过 Restful API 调用统一测试数据服务，然后由它在被测系统中实际创建或者搜索符合要求的测试数据。而具体的测试数据创建或者搜索的细节，对于测试数据的使用者来说，是不需要知道的。也就是说，统一测试数据服务，会帮助我们隐藏测试数据准备的所有相关细节。

同时，在统一测试数据服务内部，通常会引入自己的内部数据库管理测试元数据，并提供诸如有效测试数据数量自动补全、测试数据质量监控等高级功能。

在实际工程项目中，测试数据的创建通常都是通过调用测试数据准备函数完成的。而这些函数内部，主要通过 API 和数据库操作相结合的方式，实际创建测试数据。

如果你对测试数据的准备还有疑问，或者想知道更多的细节内容，可以再回顾一下前面“测试数据准备”的系列的第 35~38 篇文章。

测试执行环境准备服务

测试执行环境准备服务，其实我也已经介绍过了。这里“测试执行环境”，是狭义的概念，特指具体执行测试的测试执行机器集群：对于 GUI 自动化测试来说，指的就是 Selenium Grid；对于 API 测试来说，指的就是实际发起 API 调用的测试执行机器集群。

测试执行环境准备服务的使用方式，一般有两种：

一种是，由统一测试执行服务根据测试负载情况，主动调用测试执行环境准备服务来完成测试执行机的准备，比如启动并挂载更多的 Node 到 Selenium Grid 中；

另一种是，测试执行环境准备服务不直接和统一测试执行服务打交道，而是由它自己根据测试负载来动态计算测试集群的规模，并完成测试执行集群的扩容与收缩。

被测系统部署服务

被测系统部署服务，主要被用来安装部署被测系统和软件。虽然这部分内容我以前没有提到过，但它很好理解。其实现原理是，调用 DevOps 团队的软件安装和部署脚本。

对于那些可以直接用命名行安装和部署的软件来说很简单，一般只需要把人工安装步骤的命名行组织成脚本文件，并加入必要的日志输出和错误处理即可。

对于那些通过图形界面安装的软件，一般需要找出静默（Silent）模式的安装方式，然后通过命令行安装。

如果被测软件安装包本身不支持静默安装模式，我强烈建议给发布工程师提需求，要求他加入对静默安装模式的支持。其实，一般的打包工具都能很方能地支持 Silent 安装模式，并不会增加额外的工作量。

被测系统部署服务，一般由 CI/CD 流水线脚本来调用。在没有被测系统部署服务之前，CI/CD 流水线脚本中一般会直接调用软件安装和部署脚本。而在引入了被测系统部署服务后，我们就可以在 CI/CD 流水线脚本中直接以 Restful API 的形式调用标准化的被测系统部署服务了。这样做的好处是，可以实现 CI/CD 流水线脚本和具体的安装部署脚本解耦。

测试报告服务

测试报告服务，也是测试基础架构的重要组成部分，其主要作用是为测试提供详细的报告。

测试报告服务的实现原理，和传统测试报告的区别较大。

传统的软件测试报告，通常直接由测试框架产生，比如 TestNG 执行完成后的测试报告，以及 HttpRunner 执行结束后的测试报告等等，也就是说测试报告和测试框架绑定在了一起。

对于大型电商网站而言，由于各个阶段都会有不同类型的测试，所以测试框架本身就具有多样性，因此对应的测试报告也是多种多样。而测试报告服务的设计初衷，就是希望可以统一管理这些格式各异、形式多样的测试报告，同时希望可以从这些测试报告中提炼出面向管理层的统计数据。

为此，测试报告服务的实现中引入了一个 NoSQL 数据库，用于存储结构各异的测试报告元数据。在实际项目中，我们会改造每个需要使用测试报告服务的测试框架，使其在完成测执行后将测试报告的元数据存入到测试报告服务的 NoSQL 数据库。这样，我们再需要访问测试报告的时候，就可以直接从测试报告服务中提取了。

同时，由于各种测试报告的元数据都存在了这个 NoSQL 数据库中，所以我们就可以开发一些用于分析统计的 SQL 脚本，帮助我们获得质量相关信息的统计数据。

测试报告服务的主要使用者是测试工程师和统一测试执行服务。对统一测试执行服务来说，它会调用测试报告服务获取测试报告，并将其与测试执行记录绑定，然后进行显示。而测试工程师则可以通过测试报告服务这个单一的入口，来获取想要的测试报告。

全局测试配置服务

全局测试配置服务是这 6 个服务中最难理解的部分，其本质是要解决测试配置和测试代码的耦合问题。这个概念有点抽象，我们一起看个实例吧。

大型全球化的电商网站在全球很多国家都有站点，这些站点的基本功能是相同的，只是某些小的功能点会有地域差异（比如，因当地法务、政策等不同而引起的差异；又比如，由货币符号、时间格式等导致的细微差异）。

假设，我们在测试过程中，需要设计一个 getCurrencyCode 函数来获取货币符号，那么这个函数中就势必会有很多 if-else 语句，以根据不同国家返回不同的货币符号。

比如，如图 2 所示的“Before”代码中，就有 4 个条件分支，如果当前国家是德国（isDESite）或者法国（isFRSite），那么货币符号就应该是“EUR”； 如果当前国家是英国（isUKSite），那么货币符号就应该是“GBP”；如果当前国家是美国（isUSSite）或者是墨西哥（isMXSite），那么货币符号就应该是“USD”；如果当前国家不在上述的范围，那么就抛出异常。

图 2 全局测试配置服务的原理示例

上述函数的逻辑实现本身并没有问题，但是当你需要添加新的国家和新的货币符号时，就需要添加更多的 if-else 分支，当国家数量较多的时候，代码的分支也会很多。更糟糕的是，当添加新的国家时，你会发现有很多地方的代码都要加入分支处理，十分不方便。

那么，有什么好的办法，可以做到在添加新的国家支持时，不用改动代码吗？

其实，仔细想来，之所以要处理这么多分支，无非是因为不同的国家需要不同的配置值（这个实例中，不同国家需要的不同配置值就是货币符号），那如果我们可以把配置值从代码中抽离出去放到单独的配置文件中，然后代码通过读取配置文件的方式来动态获取配置值，这样就可以做到加入新的国家时，不用再修改代码本身，而只要加入一份新国家的配置文件就可以了。

为此，我们就有了如图 2 所示的“After”代码以及图中右上角的配置文件。“After”代码的实现逻辑是：通过 GlobalRegistry 并结合当前环境的国家信息来读取对应国家配置文件中的值。比如，GlobalEnvironment.getCountry() 的返回值是“US”，也就是说当前环境的国家是美国，那么 GlobalRegistry 就会去“US”的配置文件中读取配置值。

这样实现的好处是，假定某天我们需要增加日本的时候，getCurrencyCode 函数本身不用做任何修改，而只需要增加一个“日本”的配置文件即可。

至此，我们已经一起了解了大型全球化电商网站的全局测试基础架构设计，以及其中的 6 个主要测试服务的作用及其实现思路。现在，我再和你分享一个实例，看看这样的测试基础架构是如何工作的，帮助你进一步理解测试基础架构的本质。

大型全球化电商网站测试基础架构的使用实例

这个实例，我会以 CI/CD 作为整个流程的起点。因为，在实际工程项目中，自动化测试的发起与执行请求一般都是来自于 CI/CD 流水线脚本。

首先，CI/CD 流水线脚本会以异步或者同步的方式调用被测系统部署服务，安装部署被测软件的正确版本。这里，被测系统部署服务会访问对应软件安装包的存储位置，并将安装包下载到被测环境中，然后调用对应的部署脚本完成被测软件的安装。之后，CI/CD 脚本中会启动被测软件，并验证新安装的软件是否可以正常启动，如果这些都没问题的话，被测系统部署服务就完成了任务。

这里需要注意的是：

如果之前的 CI/CD 脚本是以同步方式调用的被测系统部署服务，那么只有当部署、启动和验证全部通过后，被测系统部署服务才会返回，然后 CI/CD 脚本才能继续执行；

如果之前的 CI/CD 脚本是以异步方式调用的被测系统部署服务，那么被测系统部署服务会立即返回，然后等部署、启动和验证全部通过后，才会以回调的形式通知 CI/CD 脚本。因此，CI/CD 脚本也要为此做特殊处理。

被测系统部署完成后，CI/CD 脚本就会调用统一测试执行服务。统一测试执行服务会根据之前部署的被测软件版本选择对应的测试用例版本，然后从代码仓库中下载测试用例的 Jar 包。

接下来，统一测试执行服务会将测试用例的数量、浏览器的要求，以及需要执行完成的时间作为参数，调用测试执行环境准备服务。

测试执行环境准备服务会根据传过来的参数，动态计算所需的 Node 类型和数量，然后根据计算结果动态加载更多的基于 Docker 的 Selenium Node 到测试执行集群中。此时，动态 Node 加载是基于轻量级的 Docker 技术实现的，所以 Node 的启动与挂载速度都非常快。

因此，统一测试执行服务通常以同步的方式调用测试执行环境准备服务。

测试执行环境准备好之后，统一测试执行服务就会通过 Jenkins Job 发起测试的执行。测试用例执行过程中，会依赖统一测试数据服务来准备测试需要用到的数据，并通过全局测试配置服务获取测试相关的配置与参数。

同时，在测试执行结束后，还会自动将测试报告以及测试报告的元数据发送给测试报告服务进行统一管理。

以上就是这套测试基础架构的执行过程了。

总结

通过前面几篇文章，我们已经掌握了测试基础架构的基础知识，所以今天我分享的主题就是，从实战的角度帮你夯实测试基础架构的基础。

其实，大型全球化电商网站全局测试基础架构的设计思路，可以总结为“测试服务化”。于是，我总结了一个比较理想的测试基础架构，应该包括 6 大服务：统一测试执行服务、统一测试数据服务、全局测试配置服务、测试报告服务、测试执行环境准备服务，以及被测系统部署服务。

其中，统一测试执行服务，本质上讲就是统一测试执行平台；统一测试数据服务，其实就是统一测试数据平台；测试执行环境准备服务，指的是狭义的测试执行环境准备。这几部分内容，我都已经在前面的文章中分享过了，如果你有任何问题，也可以再给我留言一起讨论。

而被测系统部署服务，主要是被用来安装部署被测系统和软件，这部分也很简单；测试报告服务，虽然和传统的测试报告区别较大，但也可以通过引入一个 NoSQL 数据库，以存储的测试报告元数据的方式去实现。

全局测试配置服务是这 6 个服务中最难理解的部分，其本质是要解决测试配置和测试代码的耦合问题。我通过一个具体的不同国家对应不同货币符号的例子，和你讲述了具体如何解耦。

思考题

除了我今天分享的 6 大服务以外，其实还有更多的服务可以帮助我们提升测试效能，比如全局 Mock 服务、工程效能工具链仓库等等。你还能想到有哪些与测试相关的服务吗？

感谢你的收听，欢迎你给我留言一起讨论。

43 | 发挥人的潜能：探索式测试

你好，我是茹炳晟。今天我和你分享的主题是：发挥人的潜能之探索式测试。

从今天开始，我们又要一起进入一个新的系列了：测试新技术系列。在这个系列里，我将通过 5 篇文章，和你分享软件测试领域中比较新的 5 个测试主题：探索性测试、测试驱动开发（TDD）、精准测试、渗透测试，以及基于模型的测试。这五种新的测试技术，是我精挑细选的，初衷就是希望帮你拓宽知识面、思路。

今天这次的分享，我们就先从当下很热门的探索式测试开始吧。此时，你可能已经听说过了探索式测试，也很可能还不知道什么是探索式测试。这一切都没有关系，相信你经过我今天的这次分析，总能汲取到新的知识，对探索式测试有一个全面、清晰的认识。

软件测试与招聘面试类比

在正式开始介绍探索式测试之前，我们先一起看一个工作中招聘面试的例子吧。

假设，你是面试官，现在有一个候选人要应聘你负责的这个职位。那么，你通常都会在正式面试前，先仔细了解候选人的简历，然后根据简历情况以及这个职位的要求，设计一些高质量的面试问题。当然了，你这么做的目的是，试图通过这些面试问题判断候选人与这个职位的匹配程度。

但是，在实际面试的时候，你提出面试问题之后，通常会根据面试者的回答调整接下来的问题：

如果候选人的回答符合你的预期，你可能就会结束这个话题，然后开始一个新的话题去考察候选人其他方面的能力；

如果你对候选人的回答有疑问，你很可能就会顺着候选人的回答进行有针对性的提问，这时你提出的新问题完全可能是即兴发挥的，而不是事先准备好的。

事实上，你在面试候选人的过程中，其实同时在开展候选人评估、面试问题设计、面试执行和面试问题回答评估。可以说，这个面试过程其实就是你在“探索”你的候选人，通过“探索”去判断候选人是否符合你的要求。

那么对于软件测试来说，也有非常类似的过程。

比如，你首先根据软件功能描述来设计最初的测试用例，然后执行测试；测试执行后，可能你得到的输出和预期输出不完全一致，于是你会猜测这种不一致是否可能是软件的缺陷造成的；为了验证你的想法，你会根据错误输出设计新的测试用例，然后采用不同的输入再次检查软件的输出。

在一次测试中，你可能会经过几轮这样的猜测和验证，进行反复“探索”，最终确定了一个软件的缺陷。而这个过程中，你会发现，识别缺陷的思路和测试用例的设计，并没有出现在最初的测试设计和测试用例文档中，而是以很快的速度在你的脑海中以及实际测试执行和验证中快速迭代。

从本质上来看，上述的两个过程就是探索式测试最基本的思维模型了。所以说，探索式测试本身并不是一种测试技术，而是一种软件测试风格。这个测试风格，强调测试工程师要同时开展测试学习、测试设计、测试执行和测试结果评估等一系列的活动，以持续优化测试工作。

其实，在目前的工程实践中，探索式测试发现的缺陷最多，而且发现的缺陷也很有代表性。所以，现在很多企业在探索式测试中都有较大投入。

那么，我现在就和你分享一下什么是探索式测试吧。

什么是探索式测试？

探索式测试，最早是由测试专家 Cem Kaner 博士在 1983 年提出的，并受到当时语境驱动的软件测试学派的支持。后来，Cem Kaner 博士在佛罗里达工学院的同事 James A. Whittaker，凭借着在微软和谷歌担任测试架构师和测试总监的经验积累，撰写了最早的探索式测试书籍（Exploratory Software Testing），扩展了探索式测试的概念和方法。

从本质上来看，探索式测试具有即兴发挥、快速实验、随时调整等特征， Kaner 博士在 2006 年 1 月的 Exploratory Testing Research Summit 会议上将探索式测试的定义总结为以下的一句话（在这里，我直接引用了原文）。接下来，我就和你一起解读一下其中最关键的几个概念吧。

Exploratory software testing is a style of software testing that emphasizes the personal freedom and responsibility of the individual tester to continually optimize the value of her work by treating test-related learning, test design, test execution, and test result interpretation as mutually supportive activities that run in parallel throughout the project.

首先，探索式测试是一种软件测试风格，而不是一种具体的软件测试技术。作为一种思维方法，探索式测试强调依据当前语境与上下文选择最合适的测试技术。所以，切记不要将探索式测试误认为是一种测试技术，而应该理解为一种利用各种测试技术“探索”软件潜在缺陷的测试风格。

其次，探索式测试强调独立测试工程师的个人自由和责任，其目的是为了持续优化其工作的价值。测试工程师应该为软件产品负责，充分发挥主观能动性，在整体上持续优化个人和团队的产出。这种思想方法，与精益生产、敏捷软件开发的理念高度一致，这也正是探索式测试受到敏捷团队欢迎的原因之一。

这里需要特别指出的是，探索式测试对个人的能力有很高的依赖：同样的测试风格，由不同的人来具体执行，得到的结果可能会差别巨大。因此，对执行探索式测试的工程师的要求就会比较高，除了要能够从业务上深入理解被测系统外，还要有很强的逻辑分析与推理能力，当然对测试技术以及测试用例设计的融会贯通也是必不可少的技能。

最后，探索式测试建议在整个项目过程中，将测试相关学习、测试设计、测试执行和测试结果解读作为相互支持的活动，并行执行。

注意，这里的并行（run in parallel）并不是真正意义上的并行，而是指对测试学习、测试设计、测试执行和测试分析的快速迭代，即在较短的时间内（比如，1 个小时或者 30 分钟内）快速完成多次循环，以此来不断收集反馈、调整测试、优化价值。这样，在外部看来，就会感觉这些活动是在并行地执行。

这样的理念与敏捷开发中的“小步快跑”、持续反馈的理念不谋而合，因此几乎所有的敏捷团队都会或多或少地应用探索式测试。而且，通常来讲，在敏捷开发的每个迭代中，新开发的功能基本都要依靠探索式测试保证产品的质量。

说到这里，你可能很容易陷入一个误区，那就是探索式测试看起来并没有书面严格意义上的测试设计文档，而且很多测试是在测试执行过程中即兴产生并执行的，那这样的测试风格是不是可以和即兴测试相提并论，两者又有什么区别和联系呢？

探索式测试与即兴测试的区别和联系

虽说探索式测试与即兴测试（Ad-hoc Testing）的风格看起来类似，都是依靠测试工程师的经验和直觉来即兴发挥，快速地试验被测试应用，并不停地调整测试策略。但是，探索式测试相比即兴测试更强调及时“反馈”的重要性。

在探索式测试中，测试工程师不断提出假设，通过测试执行去检验假设，通过解读测试结果证实或推翻假设。在这个迭代过程中，测试工程师不断完善头脑中被测试应用的知识体系，并建立被测应用的模型，然后利用模型、过往经验，以及测试技术驱动进一步的测试。

相比即兴测试完全不注重测试计划和设计，探索式测试要不停地优化测试模型和测试设计。由于测试设计和测试执行的切换速度很快，也就是说切换频率很高，所以会很容易传达“探索式测试没有测试计划和设计”这个错误的信息。然而，实际情况是，探索式测试有明确的测试目标和测试设计，只是测试设计的时间很短，会以很高的频率与测试执行交替切换。

掌握了探索式测试的基本理念之后，接下来我们再简单看一下探索式测试具体是如何实施的。

如何开展探索性测试？

探索式测试也是可以采用分层测试的策略。

通常，我们首先会对软件的单一功能进行比较细致的探索式测试。“探索”的过程主要是基于功能需求以及非功能性需求进行扩展和延伸，期间可以采用类似“头脑风暴”的工具，比如 Xmind 等，帮助我们整理思路。

比如，软件系统的用户登录功能就是一个单一的功能，所以作为探索式测试人员，首先应该站在最终用户的角度去理解和使用登录功能。也就是说，探索式测试人员需要了解真正的业务需求，然后基于这些业务需求“探索”软件的功能是否可以满足业务需求。

为此，探索式测试人员需要分析出用户登录功能的所有原子输入项。这里为了简化，假定原子输入项只有用户名、密码和登录按钮。接着，组合这些原子输入项构成最基本典型的测试场景。至于什么才是最基本典型的场景，则取决于探索式测试人员对需求的理解程度。

比如，用真实合法的用户名以及密码完成登录就是一个非常基本典型的场景，如果该场景能够成功登录，就可以切换到下一个；如果该场景不能够成功登录，就需要去“探索”为什么没能登录成功，比如你可能会怀疑是否是因为用户名或者密码是区分大小写的，又或者是不是因为你多次错误的尝试而导致的。

基于你的怀疑，进一步去设计新的测试用例来验证你的猜测。如果你怀疑是用户名或者密码大小写不一致造成的登录失败，那么就需要尝试使用大小写完全一致的用户名和密码进行尝试，之后还应该故意设计一些测试用例采用相同字符但是不同大小写的用户名和密码去做尝试；如果你怀疑登录失败是由于过多次的失败登录引起的，你就应该故意去设计这样的场景来观察系统的实际行为是否符合你的猜想。

总之，通过以上这样的“探索”过程，你就将测试学习、测试设计、测试执行和测试结果评估串联成了一个快速迭代的过程，并在你脑海中快速建立了登录功能的详细模型。

然后，我们往往会开展系统交互的探索式测试，这个过程通常会采用基于反馈的探索式测试方法。基于反馈的探索式测试方法，会运用所有可用的测试技术，以及基于对产品深入理解后的技术直觉，并结合上一次测试结果的反馈与分析结果，指导测试工程师下一步的测试行动。

还是以用户登录功能为例，在系统交互的探索式测试中，你就不仅要考虑单一的登录功能了，而是要考虑用户登录与系统其他功能相结合的场景。

比如，你可以尝试不登录直接访问登录后的路径去观察系统的行为；再比如，你可以尝试不登录就去查看订单状态的操作等等。这些组合场景的设计主要取决于你想要验证，或者说想要“探索”的系统功能。很多时候这些灵感来自于你之前对系统的探索而取得的系统认识，同时你的技术直觉也在此扮演了重要角色。

最后，我要特别强调一下：其实，很多时候你已经在不知不觉中运用了探索式测试的思想方法，只是你自己并不知道这就是探索式测试而已。所以，探索式测试离你并不遥远，而且实现起来也没想象中的那么难。但是，探索式测试是否可以帮你找出尽可能多的缺陷，还是取决你对系统需求的理解以及根据过往经验而积累的技术直觉。而探索式测试，只是一个测试风格，或者说一个流程方法，所以其本身并不具备任何缺陷发现能力。

总结

通过今天这篇文章，我阐述了一个基本思想是：探索式测试是一种软件测试风格，而不是一种具体的软件测试技术。

作为一种思维方法，探索式测试强调依据当前语境与上下文选择最适合的测试技术，并且强调独立测试工程师的个人自由和责任，其目的是为了持续优化其工作的价值。

探索式测试建议在整个项目中，将测试相关学习、测试设计、测试执行和测试结果解读作为相互支持的活动，并行地执行。

从中我们可以看出，探索式测试的思想方法和精益生产、敏捷软件开发的理念高度一致，这也是探索式测试颇受敏捷团队欢迎的原因之一。

思考题

如果查阅探索式测试相关的技术专著，你会发现探索式测试有很多方法和工具，从中你有哪些收获呢？如果再结合你在实际项目中使用探索式测试的经验，你又有哪些心得体会呢？

欢迎你给我留言，我们一起讨论。

44 | 测试先行：测试驱动开发(TDD)

你好，我是茹炳晟。今天我和你分享的主题是“测试先行：测试驱动开发（TDD）”。

通过上一篇文章，我们已经深入理解了什么是探索式测试，以及如何用探索式测试开展具体的测试。今天我这次分享的目的，就是和你聊聊软件测试领域中的另一个很热门的话题：测试驱动开发，也就是 Test-Driven Development，通常简称为 TDD。

听上去有些迷惑是不是？测试怎么可能驱动开发呢？在传统软件的开发流程中，软件开发人员先开发好功能代码，再针对这些功能设计测试用例、实现测试脚本，以此保证开发的这些功能的正确性和稳定性。那么，TDD 从字面上理解就是要让测试先行，这又是怎么一回事呢？

确切地说，TDD 并不是一门技术，而是一种开发理念。它的核心思想，是在开发人员实现功能代码前，先设计好测试用例的代码，然后再根据测试用例的代码编写产品的功能代码，最终目的是让开发前设计的测试用例代码都能够顺利执行通过。

这样对于开发人员来说，他就需要参与到这个功能的完整设计过程中，而不是凭自己想象去开发一个功能。他有一个非常明确的目标，就是要让提前设计的测试用例都可以顺利通过，为此，他先实现测试用例要求的功能，再通过不断修改和完善，让产品代码可以满足测试用例，可以说是“小而美”的开发过程。

所以，从本质上来讲，TDD 并不属于测试技术的范畴。那么，我为什么还要单独用一篇文章和你分享这个主题呢？因为，TDD 中通常会用到很多常见的自动化测试技术，使得测试在整个软件生命周期中的重要性和地位得到了大幅提升。

可以说，TDD 的思想和理念给软件研发流程带来了颠覆性的变化，使得测试工作从原本软件研发生命周期的最后端走向了最前端。也就是说，原本测试工作是软件研发生命周期最后的一个环节，而现在 TDD 相当于把测试提到了需求定义的高度，跑到了软件研发生命周期最前面。

那么，接下来我们就一起看看 TDD 的优势有哪些，以及 TDD 的具体实施过程。

TDD 的优势

TDD 的优势，可以概括为以下五个方面：

保证开发的功能一定是符合实际需求的。

用户需求才应该是软件开发的源头，但在实际的软件开发过程中，往往会在不知情的情况下，或者自己的主观判断下，开发出一个完全没有实际应用场景的功能。而这些没有实际应用场景的功能，却因为产品验证和测试工作介入的时机都在项目后期，所以往往在集成测试中或者产品上线后才会被发现。

比如，开发人员在实现用户注册的功能时，认为需要提供使用手机号注册的功能。但是，这个功能开发完成后，测试人员却告知开发人员这个功能用不上，或者产品上线后才发现这个功能在实际场景中完全不是必须的，因为用户可以使用邮箱注册，然后再通过绑定手机号实现手机号登陆。所以，直接用手机号注册这个功能是不需要的，真正需要的是绑定邮箱和手机号的功能。

试想一下，如果是测试驱动开发，即先根据用户的实际需求编写测试用例，再根据测试用例来完成功能代码，就不会出现这种既浪费时间、精力，又没有必要的功能了。

更加灵活的迭代方式。

传统的需求文档，往往会从比较高的层次去描述功能。开发人员面对这种抽象的需求文档，往往会感觉无从下手。但是，在 TDD 的流程里，需求是以测试用例描述的，非常具体。那么，开发人员拿到这样的需求时，就可以先开发一个很明确的、针对用户某一个小需求的功能代码。

在开发过程中，开发人员可以不断的调试这个功能，通过测试 -> 失败 - 修改 / 重构 -> 测试 -> 成功的过程，使开发的代码符合预期，而不是等所有功能开发完成后，再将一个笨重的产品交给测试人员进行一个长周期的测试，发现缺陷后再整个打回来修改，然后由此又可能会引入新的缺陷。

另外，如果用户需求有变化，我们能够很快地定位到要修改的功能，从而实现快速修改。

保证系统的可扩展性。

为了满足测试先行的灵活迭代方式，我们会要求开发人员设计更松耦合的系统，以保证它的可扩展性和易修改性。这就要求，开发人员在设计系统时，要考虑它的整体架构，搭建系统的骨架，提供规范的接口定义而非具体的功能类。

这样，当用户需求有变化时，或者有新增测试用例时，能够通过设计的接口快速实现新功能，满足新的测试场景。

更好的质量保证。

TDD 要求测试先于开发，也就是说在每次新增功能时，都需要先用测试用例去验证功能是否运行正常，并运行所有的测试来保证整个系统的质量。在这个测试先行的过程中，开发人员会不断调试功能模块、优化设计、重构代码，使其能够满足所有测试场景。所以，很多的代码实现缺陷和系统设计漏洞，都会在这个不断调优的过程中暴露出来。

也就是说，TDD 可以保证更好的产品质量。

测试用例即文档。

因为在 TDD 过程中编写的测试用例，首先一定是贴合用户实际需求的，然后又在开发调试的过程中经过了千锤百炼，即一定是符合系统的业务逻辑的，所以我们直接将测试用例生成需求文档。

这里，直接将测试用例生成需求文档的方法有很多、很简单的方法，比如 JavaDoc。

这样，我们就无须再花费额外的精力，去撰写需求文档了。

你看，TDD 真的是优势多多吧。那么，接下来我们就一起来看看实施 TDD 的具体过程。

测试驱动开发的实施过程

站在全局的角度来看，TDD 的整个过程遵循以下流程：

为需要实现的新功能添加一批测试；

运行所有测试，看看新添加的测试是否失败；

编写实现软件新功能的实现代码；

再次运行所有的测试，看是否有测试失败；

重构代码；

重复以上步骤直到所有测试通过。

接下来，我们就通过一个具体的例子，来看看 TDD 的整个流程吧。

我们现在要实现这么一个功能：用户输入自己的生日，就可以输出还要多少天到下次生日。

根据 TDD 测试先行的原则，我们首先要做的是设计测试用例。

测试用例一，用户输入空字符串或者 null：

@Test

// 测试输入空字符串 null 时，是否抛出 "Birthday should not be null or empty" 异常

public void birthdayIsNull() {

    RuntimeException exception = null;

    try {

        BirthdayCaculator.caculate(null);

    }catch(RuntimeException e) {

       exception = e;

    }

    Assert.assertNotNull(exception);

    Assert.assertEquals(exception.getMessage(), "Birthday should not be null or empty");

}

@Test

// 测试输入空字符串 "" 时，是否抛出 "Birthday should not be null or empty" 异常

public void birthdayIsEmpty() {

    RuntimeException exception = null;

    try {

        BirthdayCaculator.caculate("");

    }catch(RuntimeException e) {

        exception = e;

    }

    Assert.assertNotNull(exception);

    Assert.assertEquals(exception.getMessage(), "Birthday should not be null or empty");

}

根据这个测试用例，我们可以很容易地写出这部分的 Java 代码：

public static int caculate(String birthday) {

    if(birthday == null || birthday.isEmpty()) {

        throw new RuntimeException("Birthday should not be null or empty");

    }

}

测试用例二，用户输入的生日格式不符合 YYYY-MM-dd 的格式：

@Test

// 测试输入错误的时间格式，是否抛出 "Birthday format is invalid!" 异常

public void birthdayFormatIsInvalid() {

    RuntimeException exception = null;

    try {

        BirthdayCaculator.caculate("Sep 3, 1996");

    }catch(RuntimeException e) {

        exception = e;

    }

    Assert.assertNotNull(exception);

    Assert.assertEquals(exception.getMessage(), "Birthday format is invalid！");

}

那么，这部分的 Java 代码实现便要 catch 住 ParseException, 重新自定义错误信息并抛出异常。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

Calendar birthDate = Calendar.getInstance();

try {

    // 使用 SimpleDateFormat 来格式化输入日期值

    birthDate.setTime(sdf.parse(birthday));

} catch (ParseException e) {

    throw new RuntimeException("Birthday format is invalid!");

}

测试用例三，用户输入的生日格式正确，但是今年的生日已经过了，就应该返回离明年的生日还有多少天：

@Test

// 测试用户输入的日期晚于今年生日的情况，判断是否返回离明年的生日有多少天

public void thisYearBirthdayPassed() {

    Calendar birthday = Calendar.getInstance();

    birthday.add(Calendar.DATE, -1);

    SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");

    String date = sdf.format(birthday.getTime());

    int days = BirthdayCaculator.caculate(date);

    // 天数不应该出现负数

    Assert.assertTrue(days > 0);

}

测试用例四，用户输入的生日格式正确且今年生日还没过，返回的结果应该不大于 365 天：

@Test

// 测试用户输入的日期早于今年生日的情况，判断返回的天数是否小于 365

public void thisYearBirthdayNotPass() {

    Calendar birthday = Calendar.getInstance();

    birthday.add(Calendar.DATE, 5);

    SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");

    String date = sdf.format(birthday.getTime());

    int days = BirthdayCaculator.caculate(date);

    // 天数不应该大于一年的天数，365 天

    Assert.assertTrue(days < 365);

}

测试用例五，用户输入的生日格式正确并且是今天，返回的结果应该为 0：

@Test

// 测试用户输入的日期恰好等于今年生日的情况，判断返回的天数是否是 0

public void todayIsBirthday() {

    Calendar birthday = Calendar.getInstance();

    SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");

    String date = sdf.format(birthday.getTime());

    int days = BirthdayCaculator.caculate(date);

    Assert.assertEquals(days, 0);

}

综合上述五种测试场景，根据测试用例，我们可以编写完整的功能代码覆盖所有类型的用户输入，完整代码如下：

public static int caculate(String birthday) {

    // 首先对输入的日期是否是 null 或者是 "" 进行判断

    if(birthday == null || birthday.isEmpty()) {

        throw new RuntimeException("Birthday should not be null or empty");

    }

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    Calendar today = Calendar.getInstance();

    // 处理输入的日期恰好等于今年生日的情况

    if(birthday.equals(sdf.format(today.getTime()))) {

        return 0;

    }

    // 输入日期格式的有效性检查

    Calendar birthDate = Calendar.getInstance();

    try {

        birthDate.setTime(sdf.parse(birthday));

    } catch (ParseException e) {

        throw new RuntimeException("Birthday format is invalid!");

    }

    birthDate.set(Calendar.YEAR, today.get(Calendar.YEAR));

    // 实际计算的逻辑

    int days;

    if (birthDate.get(Calendar.DAY_OF_YEAR) < today.get(Calendar.DAY_OF_YEAR)) {

        days = today.getActualMaximum(Calendar.DAY_OF_YEAR) - today.get(Calendar.DAY_OF_YEAR);

        days += birthDate.get(Calendar.DAY_OF_YEAR);

    } else {

        days = birthDate.get(Calendar.DAY_OF_YEAR) - today.get(Calendar.DAY_OF_YEAR);

    }

    return days;

}

以上场景，每添加一个新的功能点，都会添加一个测试方法；完成新功能点的软件代码后，接着运行当前所有的测试用例，以保证新加的功能代码能够满足现有的测试需求。这就是一个典型的 TDD 过程了。但是，在实际开发场景，肯定会更复杂， 你想要用 TDD 思想写出健壮稳定的代码，就需要深入理解 TDD 中的每一步。

首先，需要控制 TDD 测试用例的粒度。如果测试用例并不是最小粒度的单元测试，开发人员就不能不假思索地直接根据测试用例开发功能代码，而应该先把测试用例分解成更小粒度的任务列表，保证每一个任务列表都是一个最小的功能模块。

在开发过程中，要把测试用例当成用户，不断分析他可能会怎样调用这个功能，大到功能的设计是用类还是接口，小到方法的参数类型，都要充分考虑到用户的使用场景。

其次，要注意代码的简洁和高效。随着功能代码的增加，开发人员为了让测试能顺利通过，很可能会简单粗暴地使用复制粘贴来完成某个功能，而这就违背了 TDD 的初衷，本来是为了写出更优雅的代码，结果反而造成了代码冗余混乱。因此，在开发 - 测试循环过程中，我们要不断地检查代码，时刻注意是否有重复代码、以及不需要的功能，将功能代码变得更加高效优雅。

最后，通过重构保证最终交付代码的优雅和简洁。所有功能代码都完成，所有测试都通过之后，我们就要考虑重构了。这里可以考虑类名、方法名甚至变量名命名，是否规范且有意义，太长的类可以考虑拆分；从系统角度检查是否有重复代码，是否有可以合并的代码，你也可以参考市面上比较权威的关于重构的书完成整个系统的重构和优化。这里我建议你阅读 Martin Fowler 的《重构：改善既有代码的设计》这本书。

总的来说，TDD 有其优于传统开发的特点，但在实际开发过程中，我们应该具体场景具体分析。

比如，最典型的一个场景就是，一个旧系统需要翻新重做，并且针对这个老系统已经有很多不错的测试用例了，这就很适合选用 TDD。

总之，我们可以通过分析当前的时间、人、方式、效果各要素来最终决定是否选用 TDD。另外，需要特别注意的是，选用 TDD 并不是测试人员或者测试部门的事情，而是需要公司层面的流程和体系的配合，也正是这种原因，虽然大家都能看到 TDD 的优势，但是在实际项目中的运用还是比较有限。

总结

今天我和你分享了测试驱动开发的核心理念，以及 TDD 的优势。

TDD 的核心思想便是在开发人员实现功能代码前，先设计好测试用例，编写测试代码，然后再针对新增的测试代码来编写产品的功能代码，最终目的是让新增的测试代码能够通过。

相对于传统软件开发流程，TDD 的优势主要包括对需求精准的把控、更灵活的迭代、促使更好的系统设计、更好的交付质量以及轻量级的文档等。

最后，我用用“用户输入自己的生日，就可以输出还要多少天到下次生日”作为例子，展示了测试驱动开发的完整流程，希望帮助你对 TDD 有更直观的认识。

思考题

在实际的工程项目中，你实际使用过 TDD 吗？如果有的话，是否可以分享一下你的实践心得？如果没有的话，你是否可以设象一下你会怎么规划和设计一个 TDD 的项目？

感谢你的收听，欢迎你给我留言一起讨论。

45 | 打蛇打七寸：精准测试

你好，我是茹炳晟。今天我和你分享的主题是“打蛇打七寸：精准测试”。

在前面的两篇文章中，我和你分享了探索式测试和测试驱动开发的概念、具体的实施方法。今天，我会继续和你分享软件测试领域中的另一个前沿话题：精准测试。

软件测试行业从最开始的手工测试到自动化测试，从黑盒测试到白盒测试，测试理念和技术都发生了日新月异的变化。现如今，几乎所有的软件公司都有一套强大且复杂的自动化测试用例，用来夜以继日地保证产品的正确性和稳定性。

然而，你有没有想过，现在你所掌握的软件测试技术和用例，真的是最准确、最适合你的产品的吗？这其中，是不是存在很多冗余的测试数据、根本用不上的测试用例、永远成功不了的测试场景？

更糟糕的是，当产品代码有更新时，你根本不知道这些更新到底影响了哪些功能，也无法精准地选取测试用例，而不得不执行完整的全回归测试。

针对这类问题，精准测试的概念在 2016 年被提了出来。所谓精准测试，就是借助一定的技术手段、通过算法的辅助对传统软件测试过程进行可视化、分析以及优化的过程。也就是说，精准测试可以使得测试过程可视、智能、可信和精准。

为了可以帮助你更好地理解，为什么要有精准测试，以及它可以解决什么问题，我在和你分享精准测试的内容时，会先和你一起分析传统软件测试正面临着哪些痛点，而精准测试又是如何解决这些痛点的。

传统软件测试的主要短板

现如今，软件产品的规模以及复杂度的发展速度，可谓超乎想象，而传统的软件测试方法，在面临这些挑战时已经表现出了些许力不从心。这些力不从心，也就是传统软件测试的短板，我归纳为了下面这五大类：

第一大短板，测试的维护成本日益升高。

当传统测试的用例逐渐增加时，需要花费越来越大的时间和人力成本，去维护一个庞大的测试用例集，以此保证产品新特性和老功能的正确性和稳定性。

而在这成千上万的测试用例中，有很多陈旧的用例已经失效了（不再能满足现有产品的测试需求了），但是整个团队还是要花费很多精力去维护这个庞大的测试用例集。

第二大短板，测试过程的低效。

随着软件功能不断丰富，相应的测试用例集也愈加庞大，这时难免会出现“杀虫剂”效应，即：测试用例越来越多，而产品的“免疫力”也越来越强。

造成这种问题的原因是，我们在测试早期已经发现了 80% 的软件缺陷，除非再花费巨大的人力和时间成本去分析和增加大批量的测试用例，否则后期新增的测试用例已经很难再发现新的缺陷了。

而精准测试可以通过对已有测试数据的跟踪分析，来定位或者缩小测试范围，以此减少发现剩下 20% 的软件缺陷的工作量。

第三大短板，缺乏有效的回归用例选取机制。

在传统测试理念中，每次添加新功能或者修复缺陷，一般都需要在产品上线前进行一轮全回归测试，哪怕这次的改动只有一行代码。但是，全回归测试的测试用例数量以及执行代价一般都比较大。

这里，我们之所以要采用全回归测试，是因为我们无法准确地知道这次的更新到底会影响哪些功能，也无法知道应该从回归测试中选取哪些必要的测试用例，无奈之下只能两眼一抹黑地执行全部用例。

第四大短板，测试结果的可信度不高。

在传统的软件测试中，测试数据的统计分析人工因素占据了绝大部分比重，由此导致测试数据本身的技术公信力不够高，进而需要依靠管理手段来保证真实的测试数据被准确地记录。

这种做法不仅可靠性差，而且执行成本高。

第五大短板，无论是白盒测试技术还是黑盒测试技术都有其局限性。

如果完全基于黑盒测试，那么注定无法深入代码实现的细节，也就无法做到有的放矢地设计测试；而如果基于白盒测试技术，为了保证代码质量，往往会采用代码级测试和代码覆盖率技术。

但是，这些测试方法都强依赖于产品代码，一旦代码发生改变，很多测试都会因此失效，因此很难适应高速迭代的开发流程。

另外，由于目前的代码级测试和代码覆盖率技术还不支持测试用例级别的覆盖率分析，而是要将所有测试结果混在一起，导致白盒测试时无法区分代码覆盖率的贡献到底来自于哪个测试用例，这将极大地限制白盒测试工具在测试结果分析上的应用。

精准测试的核心思想

而，精准测试便是为了解决传统测试的这些短板。它的核心思想是，借助一些高效的算法和工具，收集、可视化并且分析原生的测试数据，从而建立起一套测试分析系统。

所以，精准测试的主要特征可以概括为以下几个方面：

第一，精准测试是对传统测试的补充。精准测试是基于传统测试数据的，并不会改变传统的软件测试方法，更不会取代传统测试。也就是说，精准测试在不改变原有测试集的基础下，能够优化测试过程和数据，提高测试效率。

第二，精准测试采用的是黑盒测试与白盒测试相结合的模式。在执行黑盒测试时，收集程序自动产生的白盒级别的运行数据，然后通过可视化或者智能算法识别出测试未覆盖的点，继而引导开发人员和测试人员有的放矢地补充测试用例。

同时，在黑盒测试的执行过程中，可以实现测试用例和产品代码的自动关联，将基于黑盒的功能测试直接映射到基于白盒的代码层，这将使智能回归测试用例选取的想法成为可能。

第三，精准测试的数据可信度高。精准测试的数据都是由系统自动录入和管理的，人工无法直接修改数据，因此我们可以直接将传统测试产生的数据导入精准测试系统，用于测试结果的分析，从而使测试结果具有更高的可信度。

第四，精准测试过程中，不直接面对产品代码。精准测试通过算法和软件实现对测试数据和过程的采集，因此并不会直接面向代码，也就不会强依赖于产品代码。

但是，精准测试能够实现测试用例和产品代码的自动关联，也就是说代码覆盖率的统计可以以测试用例为单位来进行，具体实现的核心思想还是基于代码覆盖率的统计，只是在代码覆盖率的元数据上增加了测试用例的信息。

因此，代码的改变并不会影响测试过程，但却能够将功能测试间接映射到代码级别。这样，精准测试就实现了测试用例和被测产品代码的双向追溯。

第五，精准测试是与平台无关的、多维度的测试分析算法系统。精准测试系统是一种通用的测试分析系统，独立于任何测试平台，其内部算法和业务无关，因此适用于各种不同的产品。

同时，精准测试为我们提供了多维度的测试分析算法，拓展了白盒测试的范畴。而，精准测试对测试用例和产品代码的自动关联，使得它可以为测试过程提供大量的智能分析结果。

接下来，我们再一起看看精准测试具体有哪些方法。

精准测试的具体方法

目前业界最成熟并且已经产品化的精准测试体系，来自于国内公司“星云测试”。所以，下面关于精准测试的具体方法的分享中，涉及到的很多概念我都参考了其官网的《星云精准测试白皮书》。如果你对完整的白皮书内容感兴趣的话，可以参考http://www.threadingtest.com/index.html。

目前，由星云测试实现的精准测试平台中，核心组件包括精准测试示波器、测试用例和被测产品代码的双向追溯、智能回归测试用例选取，以及测试用例聚类分析这四项最关键的技术。在这其中，最为核心的技术是测试用例和产品代码的双向追溯。

接下来，我会依次和你分享这 4 项核心技术，希望借此加强你对精准测试的理解。

软件精准测试示波器

软件精准测试示波器，即在软件测试（人工测试或者自动化测试）的过程中，自动分析代码运行的一些数据指标，并将其用图表的方式实时显示出来。其中，这些数据指标包括了代码的逻辑块执行速率、代码的条件执行速率、函数的调用速率等等。

同时，由于示波器记录了每个测试用例的产品代码执行序列，因此可以通过比较两个测试用例的产品代码执行序列来判断两个测试用例是否隶属于同一个等价类，这将有助于精简测试用例的数量。

另外，由于示波器所有的数据都是通过系统自动导入的，因此不存在人工导入可能引入的数据误差，借此保证了所有数据的分析和显示都是真实且可靠的。

最终，示波器以类似心电图的形状实时显示测试过程中被测代码的运行信息，因此我们可以很直观地看到测试中发生的变化。一旦测试过程稍有异常，就会立刻显示在示波器上，我们通过图形的变化就可以轻易地对平时不可见的程序行为进行分析，并作出判断。比如，是否存在计算密集的区域，是否有不该执行的代码在后台运行等。

测试用例和被测产品代码的双向追溯

顾名思义，测试用例和被测产品代码的双向追溯，就是通过一定的技术手段实现测试用例和被测产品代码的双向关联。这样，我们可以通过测试用例追溯到其执行的代码，也可以通过分析代码的功能为测试提供数据。

这里，测试用例和被测代码的双向追溯，包括正向追溯和反向追溯。

其中，正向追溯，即通过示波器将产品代码和测试用例进行自动关联。这个关联，可精确到方法或者代码块级别。而在关联之后，精准测试系统可以显示每个测试用例实际执行的代码。这样，当我们发现软件缺陷时，便可以快速定位出其所在的代码。

反向追溯是指，如果我要关注程序中的某一块代码，那么就可以通过精准测试系统追溯到所有测试这块代码的测试用例。这样，就使得测试数据便于统计和量化，同时测试和开发工程师之间就可以基于测试数据进行交流，为他们的沟通提供更有效的桥梁，降低沟通成本。

这里我画了一张图，来帮助你更好地理解正向追溯和反向追溯的概念。

图 1 测试用例和被测产品代码的双向追溯示意图

总而言之，测试用例和被测产品代码的双向追溯能显著提升测试效率：

当我们发现了软件缺陷和错误时，通过这个方法可以迅速定位到有问题的代码逻辑；

当出现一些难以复现的缺陷时，这个方法可以帮助我们追溯有问题的代码而无需强行复现。

说到这里，你可能会有一个疑问，双向追溯技术后台一定是采用了代码覆盖率的统计工具，但是这个代码覆盖率统计工具和双向追溯又具体有什么区别和联系呢？

事实上，这两者之间最大的区别，体现在测试覆盖率的统计方式上。传统的代码覆盖率统计工具，会把所有测试产生的覆盖率混在一起，并不具备单个测试用例的覆盖率统计功能；而精准测试中的双向追溯技术，则可以将覆盖率的分析和计算精确到每个测试用例针对的产品代码。

另外，从实际工程的角度来看，传统的代码覆盖率统计工具都是单机运行，然后完成数据的统计，无法有效整合一个团队下多人的测试结果，也不能按照日期累计。而，现如今的双向追溯技术，则支持多人异地测试、整合计算覆盖率等功能。

智能回归测试用例选取算法

回归测试，就是在修复了某个错误或缺陷后，再对软件进行测试以确保没有引入新的错误或缺陷。而，智能回归测试用例选取算法便是针对需要执行的回归测试，通过算法得出各个测试用例的权重和优先级，使得在有限的时间和人力下，能够更高效地执行测试用例。

由于精准测试提供了智能算法来自动选取回归测试用例，因此既避免了人工选取回归测试用例时可能存在的测试盲点，也减少了执行回归测试的时间，同时还能够保证计算结果的精确性，大大降低了回归测试的风险。

另外，精准测试中测试用例和被测产品代码的双向追溯性，也使得当有代码变更需要执行回归测试时，可以直接找到具体应该执行哪些测试用例。

测试用例的聚类分析

测试用例的聚类分析，是指通过建立测试用例和代码执行的剖面关系，实现对测试用例的聚类分析。这个聚类分析的结果，将以两维数据呈现出来，即：测试用例 ID 及其对应的代码执行剖面。

通过聚类数据，我们可以很容易地发现测试用例的执行错误。比如，测试用例 A 应该执行代码块 A，而通过聚类分析，我们发现用例执行完被分在了代码块 B 上。因此，我们就可以断定该测试用例发生了错误或者测试环境出现了问题。

同时，测试用例的聚类分析能够展示测试用例的分布情况，为我们调整测试用例的分布提供依据。也就是说，我们可以通过这个数据，对测试用例聚集较少的区域予以补充丰富，同时也可以在测试用例聚集丰富的区域内提取出相对重要的用例，然后执行，从而节省时间、提高测试效率。

精准测试的概念和理论体系虽然比较完善，但就目前来看实际落地的案例还比较少。有些公司可能并不会直接去使用星云测试的平台，而是会基于精准测试的理念和方法去开发自己的工具。比如，有些国内互联网公司，就自己实现了基于增量的代码覆盖率统计方案，以及具有双向追溯功能的代码覆盖率方案。

总结

在今天这篇文章中，我和你分享了传统软件测试的方法、理念，因为测试用例数量持续增加而导致的用例维护成本高、测试过程低效、缺乏有效的回归测试用例选取机制等等一系列的问题，而有些力不从心。于是，精准测试应运而生了。

可以说，精准测试是通过一系列的智能算法和技术实现了对测试过程的管理。它可以在测试运行时，分析源数据指标以指导传统测试，并在一次次的修正中大幅提升测试效率。并且，精准测试在测试过程中产生的海量精准数据，即使不在测试周期里，也可以进行分析和追溯，让测试变得更加高效和有价值。

所以说，精准测试在节省了人力成本的同时，保证了软件的质量。

思考题

在这里，我并没有和你分享精准测试的所有概念，但是我建议你可以仔细阅读一下星云测试的《精准测试框架白皮书》。然后，你可以再思考一下，还有哪些项目和产品更适于开展精准测试。

如果你还有任何关于精准测试的疑问，欢迎给我留言一起讨论。

46 | 安全第一：渗透测试

你好，我是茹炳晟。今天我和你分享的主题是：安全第一之渗透测试。

随着互联网的发展，网络环境越来越复杂，各类软件涉及的领域也越来越多，这时系统与软件的安全问题就愈加重要了。各类隐私信息、财务信息等的泄露，稍有不慎就会造成难以挽回的损失。

所以，大多数的公司，尤其是中大型的公司，已经针对系统与软件的安全采取了很多手段。比如，安装杀毒软件、定期给系统打补丁、定期进行漏洞及安全扫描、测试并封杀应用自身的安全漏洞等等。

虽说这些措施已经可以防止大部分的安全漏洞了，但却还不足以完全保证系统的安全性。这个时候，渗透测试便以其独立的“风姿”出现在了你我的视野里。

那么，接下来我们就一起看看什么是渗透测试，以及具体如何执行渗透测试吧。

渗透测试的定义

渗透测试指的是，由专业安全人员模拟黑客，从其可能存在的位置对系统进行攻击测试，在真正的黑客入侵前找到隐藏的安全漏洞，从而达到保护系统安全的目的。

或许你会有这样的疑问，软件系统在研发阶段已经用了各种手段保证安全性，为什么还需要进行渗透测试呢？

其实，这就好比让开发人员自己做测试一样，虽说他们对自己一手开发出来的软件产品再熟悉不过了，但却也是最难测出漏洞的。因为，开发人员的惯性思维，会使得他们在面对很多的潜在问题时，都误以为这不是问题的，所以我们需要引入独立的测试人员。

同样的道理，面对自己开发的系统，开发人员总是习惯性地去处理比较容易出现安全漏洞的地方，而对于一些隐藏的、不容易被发现的漏洞，却很难发现。

另外，除了惯性思维之外，开发人员通常并不是安全领域的专家，因此往往会缺少专业的安全知识，不了解常用的系统攻击手段，从而导致他们并不能对安全相关的场景进行充分、客观的测试。

这里，为了便于理解，我们可以将软件系统比喻成一座房子。

当房子建好后，我们会为其配备防盗门、防盗窗，甚至是安全警报器等，这时我们自认为这个房子足够安全了。但，我们永远都不知道，意图侵入者会使用什么样的方式找到漏洞从而攻克我们布置的安全防线。

所以，为了保证这座房子足够安全，我们会考虑聘请外部的安全专家来进行一系列的检测。比如，检测防盗门是否足够牢固、门锁是否容易被破坏、报警器是否在发生异常时能够正常发出警报、窗户和通道是否容易被侵入，或者从根本上判定我们所布下的安全防线，在安全机制上是否存在系统性的问题，以及需不需要更新等等。

我们甚至可以找人模拟入侵这座房子，在这个模拟过程中，由其发现这所房子是否还存在安全漏洞，以此验证房子真实的安全性。

那么，这个由外部的安全专家验证房子安全性的过程，便可以说是对这座房子进行渗透测试的过程。其中，这个房子便是我们的软件系统；而我们为验证房子安全性采取的这一系列方法，就是我们所说的安全渗透测试。

渗透测试的常用方法

那么，安全渗透测试应该怎样进行呢？在这里，我总结了渗透测试的五种常用测试方法，包括：

有针对性的测试；

外部测试；

内部测试；

盲测；

双盲测试。

接下来，我们就一起看看，具体每种测试方法，要如何开展吧。

有针对性的测试

有针对性的测试，是由公司内部员工和专业渗透测试团队共同完成的。其中，公司内部员工不仅要负责提供安全测试所需要的基础信息，同时也要负责业务层面的安全测试；而专业渗透测试团队，则更多关注业务以外的、更普适的安全测试。

有针对性的测试，属于研发层面的渗透测试。参与这类测试的人员，可以得到被测系统的内部资料，包括部署信息、网络信息、详细架构设计，甚至是产品代码。

有时，我们也把这种测试方法叫作“开灯”测试。之所以称为“开灯”测试，是因为有针对性的测试，是在测试人员完全了解系统内部情况的前提下开展的，有区别于外部人员完全不知道系统内部细节而进行的渗透测试。

外部测试

外部测试，是针对外部可见的服务器和设备（包括：域名服务器（DNS）、Web 服务器或防火墙、电子邮箱服务器等等），模拟外部攻击者对其进行攻击，检查它们是否能够被入侵，以及如果被成功入侵了，会被入侵到系统的哪一部分、又会泄露多少资料。

一般情况下，外部测试是由内部的测试人员或者专业渗透测试团队，在假定完全不清楚系统内部情况的前提下开展的。

内部测试

内部测试是由测试工程师模拟内部人员，在内网（防火墙以内）进行攻击，因此测试人员会拥有较高的系统权限，也能够查看各种内部资料，目的是检查内部攻击可以给系统造成什么程度的损害。

所以，内部测试是为了防止系统的内部员工对系统进行内部攻击，同时以此来制定系统内部员工的权限管理策略。

盲测

盲测，指的是在严格限制提供给测试执行人员或团队信息的前提下，由他们来模拟真实攻击者的行为和上下文。通常，测试人员可能只被告知被测系统公开的信息，而对系统细节以及内部实现一无所知。

因为这种类型的测试可能需要相当长的时间进行侦察，所以代价会相对昂贵。而且，这类测试的效果，将在很大程度上取决于测试人员的技术水平。一般来讲，盲测是由专业渗透测试团队在测试后期开展的，通常会借助很多黑客攻击工具。

可以想象，如果测试人员拥有专业黑客的技术水平，同时结合各类渗透和黑客工具，一定能发现安全漏洞；但是，如果测试人员并不具备专业的安全测试以及系统攻击知识，那么可想而知，他们能够发现的问题就非常有限了。

双盲测试

双盲测试比盲测更进一步，也叫作“隐秘测试”。

在这类测试中，不光测试人员对系统内部知之甚少，而且被测系统内部也只有极少数人知道正在进行安全测试。因此，双盲测试可以反映软件系统最真实的安全状态，能够有效地检测系统在正常情况下，对安全事件的监控和处理能力是否合格。

因此，双盲测试可以用于测试系统以及组织的安全监控和事故识别能力，及其响应过程。一般来说，双盲测试一般是由外部的专业渗透测试专家团队完成，所以实际开展双盲测试的项目并不多。

执行渗透测试的步骤

了解了渗透测试的常用方法，那么到底要怎样具体开展呢？现在，我就和你分享一下开展渗透测试的 5 个主要步骤：

第一步：规划和侦察。这一步包含了定义测试的范围和目标、初步确定要使用的工具和方法、明确需要收集的情报（例如，网络和域名，邮件服务器），以更好地了解目标的工作方式及其潜在的安全漏洞。

第二步：安全扫描。安全扫描包括静态分析和动态分析两个阶段。

静态分析阶段，是通过扫描所有代码来估计其运行时的方式。这里，我们可以借助一些工具来一次性地扫描所有代码。目前，主流工具有 Fortify SCA 和 Checkmarx Suite。

动态分析阶段，则是在代码运行时进行扫描。这样的扫描更能真实反映程序的行为，可以实时提供应用程序的运行时视图，比静态扫描更准确、实用。

第三步：获取访问权限。在这一步，测试人员将模拟黑客对应用程序进行网络攻击，例如使用 SQL 注入或者 XSS 跨站脚本攻击等，以发现系统漏洞。然后，利用找到的漏洞，通过升级自己的权限、窃取数据、拦截流量等方式了解其可能对系统造成的损害。

至于到底什么是 SQL 注入，什么是 XSS 跨站脚本攻击，你可以自行查阅一些资料，也可以给我留言一起讨论。

第四步：维持访问权限。这个阶段的目的是，查看被发现的漏洞是否可以长期存在于系统中，如果漏洞能够被持久化，那么在很长的一段时间内入侵者都可以对系统进行深入访问或进行破坏。

这个阶段模仿的是高级持续性威胁。这类威胁，通常在系统中可以存在数月之久，入侵者可以借此获取组织内较高级别的敏感数据。

第五步：入侵分析。完成以上的四步之后，我们就要分析得到的结果了。通常情况下，我们需要将测试结果汇总成一份详尽的测试报告，并详细说明：

可以被利用的特定漏洞；

利用该漏洞的具体步骤；

能够被访问的敏感数据；

渗透测试人员能够在系统中不被侦测到的存在时间。

专业的安全人员会分析这些信息，以指导和帮助我们配置企业的 WAF(Web Application Firewall)，同时提供对其他应用程序的安全解决方案，以修补安全漏洞并防范未来的恶意攻击。

渗透测试的常用工具

目前，在实际的渗透测试中，我们通常会使用大量的工具来完成测试。为此，我挑选了 Nmap、Aircrack-ng、SQLmap、Wifiphisher、AppScan 这五种常用工具，和你分享一下它们的功能，以及适用的场景。

这里需要特别注意的是，这些工具本身就具有黑客属性，所以很多杀毒软件会阻止该类软件的运行。同时，你也一定不要在非官方的网站下载和使用这类工具，以防被意图不轨的人预先注入了危险的攻击点，请务必小心。

Nmap

Nmap 是进行主机检测和网络扫描的重要工具。它不仅可以收集信息，还可以进行漏洞探测和安全扫描，从主机发现、端口扫描到操作系统检测和 IDS 规避 / 欺骗。

Nmap 这类工具是渗透测试过程中最先要用到的，用来获取后续渗透测试过程中需要用到的系统基本信息，比如 IP 和端口等。

同时，Nmap 适用于各大操作系统，包括 Windows、Linux、OSX 等，因此是一款非常强大、实用的安全检测工具。

Aircrack-ng

Aircrack-ng 是评估 Wi-Fi 网络安全性的一整套工具。它侧重于 WiFi 安全的领域，主要功能有：网络侦测、数据包嗅探、WEP 和 WPA/WPA2-PSK 破解。

Aircrack-ng 可以工作在任何支持监听模式的无线网卡上并嗅探 802.11a、802.11b、802.11g 的数据。

Aircrack-ng 的执行是通过命令行或者脚本文件的方式，并且可以运行在 Linux 和 Windows 操作系统上。它的典型应用场景，主要包括数据包注入重播攻击、解除身份验证、虚假接入点等，也可以用于破解 WEP 和 WPA PSK。

sqlmap

sqlmap 是一种开源的基于命令行的渗透测试工具。它能够自动进行 SQL 注入和数据库接入，并且支持所有常见并广泛使用的数据库平台，包括 Oracle、MySQL、Microsoft SQL Server、SQLite、Microsoft Access、IBM DB2、FireBird、Sybase 和 SAP Max DB 等，使用的 SQL 注入技术也几乎涵盖了所有的攻击手段。

如果你不采用 AppScan 这类全面的商用安全测试工具，我的建议是通过 sqlmap 来确保系统数据库的安全性。

Wifiphisher

Wifiphisher 是一种恶意接入点工具，可以对 WiFi 网络进行自动钓鱼攻击。

渗透测试执行人员，可以通过 Wifiphisher 执行有针对性的 WiFi 关联攻击，轻松实现无线客户端的渗透测试。

Wifiphisher 还可以用于对连接的客户端进行受害者定制的网络钓鱼攻击，用来获取凭证（例如，从第三方登录页面或 WPA/WPA2 预共享密钥）或用恶意软件感染受害者站点。

AppScan

AppScan 是 IBM 公司的一款企业级商业 Web 应用安全测试工具，采用的是黑盒测试，可以扫描常见的 Web 应用安全漏洞。

AppScan 的工作原理是：

首先，从起始页爬取站下所有的可见页面，同时测试常见的管理后台；

然后，利用 SQL 注入原理测试所有可见页面，是否在注入点和跨站脚本攻击的可能；

同时，检测 Cookie 管理、会话周期等常见的 Web 安全漏洞。

AppScan 的功能十分强大，几乎涵盖了目前所有已知的攻击手段，而且攻击库还在不断地升级更新。此外，从 AppScan 的扫描结果中，我们不仅可以看到扫描的漏洞，还提供了详尽的漏洞原理、修改建议、手动验证等。

可以说，AppScan 是目前最完美的渗透测试商用解决方案，但是其最大的问题在于其价格昂贵，一般只有中大型的企业才会购买使用。

渗透测试的收益

现在，你已经清楚了开展渗透测试的必要性，也大致清楚了具体要如何开展渗透测试。那么，接下来，为了让你对开展渗透测试的信心更足，我再为你总结一下它能解决的问题：

通过渗透测试，公司可以识别出主要漏洞，并决定修补漏洞的优先级，同时合理分配系统补丁安装的时间，以确系统环境的安全性。

避免了安全漏洞，也就是避免了不必要的损失。因为，从安全漏洞中恢复出来，公司往往要花费巨大的代价去补救公司和客户的损失，甚至可能因此吃官司。而，渗透测试能够很好地避免这类问题，帮助公司树立良好的企业形象，因此赢得更高的信任度。

总的来说，我们应该按需选择适合自己产品的渗透测试方案，期间需要考虑到产品安全隐患和执行渗透测试的成本之间的平衡。

总结

在今天的这次分享中，我介绍了与渗透测试相关的知识点。

首先，渗透测试是指由专业安全人员模拟黑客，从其可能入侵的位置对系统进行攻击测试，以达到在真正的黑客攻击之前找到隐藏的安全漏洞，从而保护系统安全的目的。

然后，我根据发起渗透测试的位置以及对系统信息的掌握程度，将渗透测试分为了有针对性的测试、外部测试、内部测试、盲测和双盲测试这五种。

接着，我和你分享了开展渗透测试的 5 个步骤，分别包括了规划和侦察、安全扫描、获取访问权限、维持访问权限，以及入侵分析。

最后，我给你汇总了五款常用的渗透测试工具，其中功能最强大的要数 IBM 的 AppScan 了，但是其价格比较昂贵，适用于中大型企业。而关于如何选择适合自己的渗透测试方案，我的建议还是要综合考虑产品安全隐患和执行渗透测试的成本。

思考题

你所在的公司或者团队是否开展了渗透测试？你们会使用哪些渗透测试工具呢？

感谢你的收听，欢迎你给我留言一起讨论。

47 | 用机器设计测试用例：基于模型的测试

你好，我是茹炳晟。今天我和你分享的主题是：用机器设计测试用例之基于模型的测试”。

我在前面 4 篇文章中，和你分享的探索式测试、测试驱动开发 TDD、精准测试，以及渗透测试的内容，你是否已经掌握了呢？有没有尝试将这些比较新的理念用到你的工程项目中呢？如果你在应用的过程中，遇到了任何问题，也欢迎给我留言一起讨论。

那么，现在我们就正式开始测试新技术系列的最后一个话题：基于模型的测试。

可以说，软件测试是一款软件产品质量的最后一道防线，是产品上线前必不可少、最重要的一个环节。每一款高质量的软件产品背后，都蕴含了大量的测试工作。而且，这些测试工作很可能是整个软件开发过程中最昂贵、劳动最密集的工作。

虽说从最简单的功能性黑盒测试，到涉及定理证明的复杂测试，已经有很多种方法可以帮助我们提高测试的可靠性和有效性。但是，在设计测试用例的过程中，总还是存在着这样那样的问题，使得软件测试的结果没那么理想。

为此，我们新引入了基于模型的测试，即 Model-Based-Testing，简称 MBT。

MBT，是自动化测试的一个分支。它是将测试用例的设计依托于被测系统的模型，并基于该模型自动生成测试用例的技术。其中，这个被测系统的模型表示了被测系统行为的预期，也可以说是代表了我们对被测系统的预期。

从质量保证的角度来看，我们可以制定测试内容，但却无法保证测试会覆盖所有可能的组合。而 MBT 则允许软件开发人员和测试人员，只关注建立系统的正确性以及模型的规范性，再通过专门的 MBT 工具根据不同的测试用例设计策略从系统模型生成可靠的测试用例。

那么，MBT 的原理是什么，而什么样的应用又适合进行 MBT 呢？接下来，我就重点为你回答这两个问题。

MBT 的基本原理

MBT 的基本原理是通过建立被测系统的设计模型，然后结合不同的算法和策略来遍历该模型，以此生成测试用例的设计。

我用下面的一张图片，为你描述了 MBT 的过程：

图 1 MBT 的一般过程

如图 1 所示，开发者首先根据产品需求或者说明来构建模型，然后结合测试对象生成测试用例，测试用例针对测试对象执行完之后，生成测试报告比对测试结果。

接下来，我以简单的登录系统为例，和你说明如何建模。

当用户访问网站时，网站需要识别用户是否已经登录：

如果已经是登录状态，则让用户进入，结束这一分支；

如果用户还没有登录，那么页面需要返回登录框给用户。用户在登录框输入用户名和密码后，由后台服务验证用户名和密码是否正确，如果通过验证，则用户登录成功，结束分支；否则，返回错误信息，并再次返回登录框供用户登录。

根据这个逻辑，我们可以建模如下：

图 2 网站登录系统建模

至此，我们就完成对这个登录系统的建模工作了。然后，我们通过具象化被测产品的需求行为，再通过工具来遍历模型中的各个路径，就可以得到我们需要的测试用例了。

所以，执行 MBT 的过程就好比你把软件系统的设计画为了一张由节点和边构成的数据结构意义上的“图”，然后通过一定的算法（比如，深度遍历或者广度遍历）来尽可能完整覆盖图中全部的可能路径的过程。

而根据被测系统的特点，我们可以创建不同类型的模型完成 MBT。接下来，我们就一起看看有哪些常用的 MBT 模型吧。

常用模型简介

根据被测系统本身的特点，我们常用的模型主要有限状态机、状态图，以及 UML 三种。其中，有限状态机和状态图比较适合于用状态或者事件驱动的系统，而 UML 比较适合于靠业务流程驱动的系统。

第一，有限状态机。

有限状态机可以帮助测试人员根据选中的输入来评估输出，不同的输入组合对应着不同的系统状态。

在登录系统这个例子中，员工在未登录时的状态是“未登录”，一旦登录成功状态就会变为“已登录”。在已登录的状态下，员工可以访问各类资源，使用系统内的工具。

第二，状态图。

状态图是有限状态机的延伸，用于描述系统的各种行为，尤其适用于复杂且实时的系统。

状态图有一定数量的状态，系统的行为可以以事件的方式来驱动状态的变化。比如，缺陷管理工具中出现了缺陷，其初始状态为“new”；缺陷被开发人员修复后，就必须将其改为“Fixed”；但是，如果此时测试人员发现缺陷并未修复或者只是部分修复时，则需将状态更改为“Reopen”（重新打开）。

状态图的设计方式，要求为每个不同的状态创建一个事件。

第三，UML。

UML 即统一建模语言，是一种标准化的通用建模语言。UML 有自己定义的图形库，里面包含了丰富的图形用以描述系统、流程等。

UML 可以通过创建可视化模型，来描述非常复杂的系统行为。

当我们完成被测系统的建模工作后，接下来就要将模型转化为可执行的测试用例了。这个转换过程，需要借助工具来完成。

因为不同领域的产品风格迥异，其使用的自动化框架和编程语言也各不相同，所以我们需要花费一些精力去寻找与自己产品匹配的 MBT 工具。其实，在很多情况下，我们还需要根据产品特点，去自行开发和定制工具。

MBT 工具简介

这里，我为你罗列了一些常见的 MBT 工具，包括：BPM-X、fMBT、GraphWalker。在这里，我为你简单介绍这些工具的目的是，让你可以对 MBT 工具本身有个感性的认识，让你知道此类工具的应用场景和上下文。至于说如何来选择使用这些工具，这在很大程度上取决于被测系统的特点。

第一，BPM-X

BPM-X 根据不同的标准（比如，语句、分支、路径、条件）从业务流程模型创建测试用例。

它还可以从多个建模工具导入模型，并可以将测试用例导出到 Excel、HP Quality Center 等。这个工具，适用于业务流程比较清晰直观的场景。

第二，fMBT

fMBT 是一组免费的、用于全自动测试生成和执行的工具，也是一组支持高水平测试自动化的实用程序和库，主要被应用在 GUI 测试中。

fMBT 包括用于多平台 GUI 测试的 Python 库，用于编辑、调试、运行和记录 GUI 测试脚本的工具，以及用于编辑和可视化分析测试模型和生成的测试工具。

第三，GraphWalker

GraphWalker 以有向图的形式读取模型，主要支持 FSM、EFSM 模型。它读取这些模型，然后生成测试路径。GraphWalker 除了适用于 GUI 测试外，更适合于多状态以及基于事件驱动的状态转换的后台系统。

另外，GraphWalker 还支持从有限状态机中生成测试用例。

除此之外，市面上还有很多 MBT 测试工具，比如 GSL、JSXM、MaTeLo、MBT Suite 等。这里就不再一一介绍了，你可以自行百度了解它们的特点和适用场景，从而选取合适自己的工具。

MBT 的优势

其实，MBT 并不能算是一种新颖的测试技术，早在七八年前就已经被提了出来并且试图应用于软件产品的测试工作中。但是，MBT 在很长一段时间内，却一直停留在概念阶段，主要原因是一直没有普适的工具支持，所以很少有成功实施的实际案例。同时，业界一直以来都缺乏高效的测试用例设计生成策略，所以虽然大家都能看到 MBT 的优势，但能在实际项目中应用执行的却是寥寥无几。

与传统测试相比，MBT 的优点如下：

测试用例的维护更轻松。由于测试用例是基于被测系统的模型生成的，因此我们只需维护好模型即可，而无需关注测试用例的细节。

软件缺陷发现得更早。由于我们在构建被测系统模型的过程中，已经对被测系统有了比较全面的理解，加之要根据系统需求 / 说明完成建模过程，所以我们可以在早期建模时发现被测系统可能存在的明显缺陷，而不用等到执行了大量的测试用例以后才发现这些缺陷。

测试自动化的水平更高。由于 MBT 只需建好模型便可以自动生成测试用例，所以不再需要人工编写测试文档。而更高级的应用，甚至可以直接生成可以直接执行的自动化测试脚本。

测试覆盖率变得更高，使得彻底的测试（即：穷尽测试）成为了可能。由于我们需要做的只是正确、详尽地用模型描述被测系统，而生成测试用例完全由 MBT 工具实现，所以这就避免了人工设计测试用例时的思维局限性，能够有效地提高测试覆盖率，让彻底的测试变为可能。
当然，是否有必要开展彻底的测试还是要由风险决定。这里的风险指的是，由于漏测导致产品问题对业务的影响程度。MBT 只是从技术上提供了可能性。

基于模型间接维护测试用例的方式更高效。在传统测试中，如果被测系统的流程或者功能发生了变化，我们需要耗费大量的人力和时间成本，去重新设计与之匹配的测试用例。而在 MBT 中，我们只需要更新被测系统的模型即可，剩下的测试用例生成工作可以由 MBT 工具自动完成。

MBT 的劣势

虽然 MBT 相对于传统测试有很多优点，但它也不是完美的测试方案。在实际开展 MBT 时，我们往往需要应对很多挑战，并克服很多困难。所以，到现在为止，MBT 并没有被广泛使用于软件测试领域。

在这里，我总结了开展 MBT 的三大难点：

学习成本较高。MBT 要求开发人员和测试人员都精通建模，这就需要一定的培训成本，需要让开发人员去学习测试的技能，让测试人员去学习建模概念。这其中还牵涉到建模工具的选择，以及学习等成本。

使用 MBT 的初期投资较大。在很多情况下，我们并不能找到适合自己产品的建模工具，而需要自行创建 MBT 工具。
在自行定制 MBT 工具时，我们要考虑到这个工具必须是可扩展的，并且能够处理复杂的测试逻辑，提供足够高的测试覆盖率，因此刚开始的工具建设就需要花费大量时间和精力。
更糟糕的情况是，当工具建好后，我们却发现它并不能满足所有的建模需求，因此还要在建模的同时对工具进行微调。而，这种微调工作的难度，也比较大。

早期根据模型生成测试用例的技术并不是非常成熟。很多时候只是根据图论的算法来遍历模型，这就会导致生成的很多测试用例在业务上根本没有任何实际意义，也因此阻碍了 MBT 在实际项目中的落地。
不过好在近一两年来，基于人工智能（AI）生成测试用例的概念不断成熟，所以将基于 AI 的测试用例生成和 MBT 相结合，将会是接下来的一个发展方向。

总的来说，MBT 和传统测试各有优劣。所以，测试的方法多种多样，MBT 只是其中的一种。

如果一个应用的任何组件都可以通过模型来模拟、通过驱动程序来驱动，并可以通过测试结果来比较的话，那么这个应用就是 MBT 的最佳候选者。

如果我们的产品特征符合开展 MBT 的要求，并且团队各方面的条件都支持使用 MBT 时，我们便可以尝试用这种方法来改革我们的测试方式。尤其是将 MBT 结合基于 AI 的测试用例生成技术，将可以大大加速 MBT 产业应用的步伐。

但是，不管是否采用 MBT，开发人员或测试人员在接触到一款软件产品时，首先都会有一个心理建模的过程，自己先去理解并在脑中勾勒出系统的功能结构和流程。其实，这些内容很容易就可以转换成实际的模型，也就为 MBT 创造了条件。

总结

今天，我和你分享了 MBT 的基本概念和方法。

MBT 是一种基于被测系统的模型，由工具自动生成测试用例的软件测试技术。所以，这也就决定了 MBT 相对于传统测试技术可以在测试用例维护、软件缺陷发现时机、测试自动化水平，以及测试覆盖率等方面有其独到的优势。

但同时，这也使得 MBT 相对于传统软件测试，在初期阶段投入较大，学习应用的成本较高。也正因为如此，MBT 概念虽然已经提出了七八年的时间，但却并没有被广泛应用于软件测试领域。

所以，关于是否要在自己的项目中开展 MBT，我们需要综合考虑项目本身的特点和人员的技术水平，以此决定是否有必要开展 MBT。

思考题

假如你要在项目中开展 MBT，你会如何判断你的项目是否适合采用 MBT，以及你认为会遇到哪些问题可能会阻碍 MBT 的开展呢？

感谢你的收听，欢迎你给我留言一起讨论。

48 | 优秀的测试工程师为什么要懂大型网站的架构设计？

你好，我是茹炳晟。今天我和你分享的主题是：优秀的测试工程师为什么要懂大型网站的架构设计？

上周我准备了两期答疑文章，希望可以帮助你解决在阅读前 11 篇文章时的一些疑问。今天，我们一起回到正题上来，讨论一下互联网的架构设计这个话题。

在这个话题里，我会和你分享测试工程师为什么要具备架构知识、怎么学架构知识，以及学到什么程度就可以了。同时，我会针对网站架构设计中最关键的四个主题：高性能架构设计、高可用架构设计、伸缩性架构设计，以及可扩展性架构设计，和你分享一些案例，让你切实体会到懂得架构知识在测试范围确定和用例设计等方面的重要性。

为什么要懂得网站的架构设计？

其实，如果你是工作在传统软件企业的工程师的话，网站的架构设计知识对你来说可能没那么重要。因为，你的测试对象是传统软件，此时你需要对你的被测软件的架构有比较深入的理解。

而现在如你所知，互联网企业已经占据软件产品的大半壁江山。如果你想跳出传统软件产品测试这个舒适区的话，那互联网企业将是一个最可能的去向。

而在互联网企业进行软件测试的话，很多时候需要针对互联网的架构来设计有针对性的测试，另外对于互联网的压力测试以及结果分析也需要对架构知识有比较清楚的认识。这时，不懂得网站架构设计知识，在开展测试时，就真的会有处处被掣肘的感觉了。更别提，这还会直接影响到你的能力提升和职业发展了。

在测试过程中，你可能会经常遇到诸如负载均衡器、缓存集群、数据库读写分离、消息队列、CDN、反向代理服务器和分布式数据库等概念，在测试执行中也经常会和这些系统打交道。但是，很多时候，你只是知道网站在架构设计上有这些组件，并不清楚这些组件真正的作用，在对应的测试设计时也很难做到“有的放矢”。

还有些时候，特别是性能测试，如果你不清楚详细的架构设计以及其中的技术细节，你可能根本无去解读和分析性能测试的报告。

我来举两个实际的例子吧。

基于消息队列的分布式系统测试设计的例子

在分布式系统的架构中，为了减少各个应用系统之间的直接耦合，往往会引入消息队列来实现解耦。

也就是说原本的功能实现是由系统 A 调用系统 B 来完成业务功能的，而引入了消息队列后，系统 A 不会再直接去调用系统 B，而是将调用 B 所需要的数据放到了消息队列中，此时我们将系统 A 称为消息的生产者，然后系统 B 通过监听该消息队列，主动从消息队列中依次抓取数据进行系统 B 的处理，系统 B 在这种情况下称为消息的消费者。

通过这种方式，就完成了系统 A 和系统 B 之间的调用解耦。

那么，我们再来看测试的设计。测试用例的设计可以站在黑盒测试的视角，完全不需要知道消息队列的存在，而直接从业务功能的层面去设计用例。

但如果只这么做的话，你会发现虽然你的测试全部通过了，但是产品一旦到了线上，还可能会出现很多问题。

比如说，消息的生产者产生消息的速度远远大于消息消费者处理消息的速度时，很可能会造成消息队列满的情况，此时系统的行为是怎么样的？

显然仅仅通过黑盒测试很难完成系统性的、全面的测试。要做到系统性全面的测试设计，你就必须知道消息队列的基本原理，然后在此基础上去设计针对具体架构的测试用例和场景。

另外，既然我们的系统设计希望是解耦的，那么我们的测试设计也希望是解耦的。也就是说，对于一些更详细的测试，我们希望系统 A 和系统 B 可以被单独的进行测试。

那么，这个时候，如果是对系统 A 进行测试的话，你的测试验证就需要在消息队列中进行。同样的道理，如果你是对系统 B 进行测试的话，你就需要在消息队列中构造测试输入数据了。

由此可见，如果你不知道消息队列的存在以及其基本原理的话，你的测试将寸步难行。

缓存的例子

很多时候，在我们搭建完性能测试的基准环境，开始执行性能基准测试的时候，往往会发现系统刚开始运行时业务处理的响应时间都会相对比较长，只有当性能测试执行了一段时间后，系统的各项指标以及事务的响应时间才逐渐趋于正常。

为此，在做性能基准测试的时候，有经验的工程师通常都会先用性能场景对系统进行一下“预热”，然后再真正开始测试。你有想过这其中的原因吗？

另外，在做前端性能测试的时候，我们对于一个页面的打开时间通常会去统计两个指标，一个是首次打开时间，另一个是多次打开的时间。而且，通常来讲首次打开时间会远大于后面再次打开的时间。你有想过这其中的原因吗？

其实，造成上述两种情况的背后原因都是采用了缓存技术。

造成第一个情况的原因，是服务器端会对“热点”数据进行缓存，而不是每次访问都直接从数据库中获取数据。那么，系统刚开始运行时，由于没有任何之前的访问记录，所有数据都需要访问数据库，所以前期的事务响应时间都会比较长。但是，随着缓存的建立，后续的访问就会比较快了。这个前期对系统的“预热”过程其实是在“预热”缓存。

对于第二种情况也是同样的道理。浏览器端也会缓存从服务器端拿到各种静态资源，在第一次访问时这些资源都需要从服务器端获取，而后面再访问时，这些静态资源已经在浏览器的缓存中了，所以访问速度会大大加快。

由此可见，如果不知道缓存的存在、不理解缓存的基本原理，你就不可能从根本上理解性能测试的方法设计以及测试结果数据。

其实，对于缓存还有很多需要考虑的测试点，但是需要解释这些测试点就需要深入理解缓存的原理以及缓存的架构设计，因为在互联网环境下，缓存本身也是分层的，浏览器端有本地缓存、网络端有 CDN 缓存、数据中心前端有反向代理的缓存、应用服务器端有本地缓存，对于大规模互联网应用更有大规模的专用缓存服务器集群。所以，要有针对性的设计缓存相关的测试场景，就需要理解这些缓存的架构。

那么，接下来我就和你聊聊作为测试工程师应该怎么学习架构知识。

测试工程师怎么学架构知识？

其实，对于测试工程师来说，学习软件架构和系统架构知识的确是个不小的挑战。因为很多架构知识都是基于开发框架和系统设计的，对开发工程师来说，已经是个不小的挑战，对测试工程师来说更是一个难以驾驭的领域。

不过好在，同样是对架构知识的学习和掌握，不同角色的工程技术人员都有不同的视角，需要了解和掌握的全局知识和细节程度也各不相同。以消息队列知识为例：

如果你是系统架构师，那么你就不仅要掌握各个不同消息队列实现的技术细节，还清楚不同方案的优势和劣势，最关键的是能够根据业务的应用场景和特点来选择最合适的消息队列方案。

如果你是软件开发人员，那么你就需要掌握消息队列的使用方法、消息 push 和 pull 的模式，以及在应用中如何以异步方式来对消息进行妥善处理，并且还要考虑到异常场景的处理。

而作为软件测试人员，你需要知道消息队列的基本原理以及在被测系统中的部署情况，同时应该知道如何访问消息队列或者队列中消息的情况。在需要模拟消息进行解耦测试的场合，你还需要知道如何添加测试消息以满足测试的目的。

可见，对于测试人员来讲，学习架构知识应该有自己独特的视角，基本只要做到清楚原理、了解在被测系统中的部署架构，从测试的角度能够调用必要的接口就可以了。

那么，我们到底应该怎么来学习架构知识呢？根据我的个人经验，我认为应该遵循“由广度到深度”和“自上而下”两个基本原则。

“由广度到深度”中的“广度”是指在平时工作以外的时间中，应该多注重全领域架构知识的积累，此时那些系统性地介绍架构知识的书籍或者专栏就可以给你最大程度的帮助了。

因为这类资料往往已经对纷繁复杂的架构知识做了系统性地梳理。这里，我个人非常推荐极客时间李运华老师的“从 0 开始学架构”专栏，以及李智慧老师所著的图书《大型网站技术架构：核心原理与案例分析》。它们都能帮你从广度上积累架构知识。

“由广度到深度”的“深度”是指，对于架构中某一领域的特定知识在项目中要实际使用的时候，必须要刨根问底，通过实际的测试来加深对架构知识细节的理解。

“自上而下”是指，在实际测试项目中，当需要设计涉及架构的测试用例和场景的时候，千万不要直接基于“点”来设计测试，而是应该：首先通过全局阅读理解上层架构设计；然后，在理解了架构设计的初衷和希望达成目的的基础上，再向下设计测试场景和用例。

这个过程，一方面可以帮你设计出有针对性的测试用例，另一方面可以帮助你理解架构在实际项目中是如何落地的。

随着你经历的项目越来越多，你的架构知识就会逐渐充实丰满起来。这就好比你在走一个旋转楼梯，一直感觉自己在原地打转，但是不知不觉走了一段时间后你回头往下看的时候，就会发现已经站在了比原来更高的点上。

最后，我再特别提一下，对于架构知识的学习没有任何捷径可走，你必须一步一个脚印，才能达到下一个高峰。

总结

今天我通过消息队列和缓存两个实例给你讲解了测试工程师学习架构知识的重要性，并且从我个人的经验出发，提出了“由广度到深度”和“自上而下”的架构学习思路，最后指出了学习架构没有捷径，你必须一步一个脚印夯实自己的知识结构。

思考题

对于架构知识的学习，我只是给出了我的一些方法和意见，你对此还有什么其他的想法或者学习方法吗？

感谢你的收听，欢迎你给我留言。

49 | 深入浅出网站高性能架构设计

你好，我是茹炳晟。今天我和你分享的主题是：深入浅出网站高性能架构设计。

在上一篇文章中，我从全局的角度和你分享了测试人员学习架构知识的重要性、应该学到什么程度，以及怎么学的问题，希望你可以借此明白网站架构的 why、what、how。

接下来，我将站在测试人员的视角，通过这个专栏的最后四篇文章，和你分享网站的高性能架构、高可用架构、伸缩性架构以及扩展性架构。希望借此机会，可以让你对网站的架构设计做到心中有数，在设计测试用例时可以做到有的放矢。

今天我们就先从网站高性能架构的设计开始吧。

性能是网站的重要指标，如果一个网站的访问速度很慢，就会直接导致大量用户的流失。所以说，性能是设计网站架构时要考虑的关键因素。也因此，网站的性能问题成了网站架构升级、优化的导火索。

目前，为了优化网站性能，业界出现了很多相关的架构改进方案和技术手段。而包括了这些升级、优化网站性能的方案、技术手段在内的高性能架构设计，是个很大的话题，单单依靠几篇文章是很难讲清楚的。所以，我从中精选了一些对测试工程师比较关键的概念和技术，和你展开今天的分享。

如果你想了解更细节的技术实现的话，可以参考我在上一篇文章中推荐的学习资料，也可以直接在留言区给我留言。

从全局来看，网站的高性能架构设计包括两大部分内容：一是前端性能，二是后端服务器相关的性能优化和架构设计。

前端的高性能架构

关于什么是前端性能，以及如何设计针对前端性能的测试，你可以直接参考第 31 篇文章《工欲善其事必先利其器：前端性能测试工具原理与行业常用工具简介》中的相关内容。

相对来说，前端高性能架构比较直观易懂，其本质就是通过各种技术手段去优化用户实际感受到的前端页面展现时间。

目前，业内的标准实践是来自于雅虎前端性能团队提出的 35 条原则，我已经在第 29 篇文章《聊聊性能测试的基本方法与应用领域》中，为你解读了其中几个比较典型的规则，你可以再回顾下。同时，你还可以访问雅虎网站查看经典的 35 条规则，以及对各规则的详细解读。

前端的高性能架构相对于后端来讲比较容易实现，因为前端性能优化的方法是相对标准的。而且，目前的前端性能测试工具，比如我在前面文章中曾经介绍过的 WebPageTest 和 YSlow 之类的工具等，都能系统性地分析前端的性能问题，并给出对应的解决方案建议。

可以说，我们只要在项目开发过程中，把前端性能优化纳入了测试范围，那么一般来讲都能获得比较理想的性能优化结果。

后端服务器的高性能架构

后端服务器的高性能架构，业内采用的最主要的技术手段是缓存。同时，集群也可以从计算能力的角度，提升后端的处理性能。

缓存

可以说，在计算机的世界中，凡是想要提高性能的场合都会使用到缓存的思想。缓存是指将数据存储在访问速度相对较快的存储介质中，所以从缓存中读取数据的速度更快。

另外，如果缓存中的数据是经过复杂计算得到的，那么再次使用被缓存的数据时，就无需再重复计算即可直接使用。从这个意义上讲，缓存还具有降低后端运算负载的作用。

可见，缓存在软件系统和网站架构中几乎无处不在。当然了，在系统和软件的不同级别对应有不同层级的缓存：

浏览器级别的缓存，会用来存储之前在网络上下载过的静态资源；

CDN 本质也是缓存，属于部署在网络服务供应商机房中的缓存；

反向代理服务器本质上同样也是缓存，属于用户数据中心最前端的缓存；

数据库中的“热点”数据，在应用服务器集群中有一级缓存，在缓存服务集群中有二级缓存；

甚至是用于 URL 和服务器 IP 地址转换 DNS 服务器，为了减少重复查询的次数也采用了缓存。

启用了缓存后，当应用程序需要读取数据时，会先试图从缓存中读取：

如果读取成功，我们称为缓存命中，此时就可以在很大程度上降低访问数据库的时间开销。

如果没有读取到数据或者缓存中的数据已经过期失效，那么应用程序就会访问数据库去获取相应的数据。获取到数据后，在把数据返回给应用程序的同时，还会把该数据写入到缓存中，以备下次使用。

缓存主要用来存储那些相对变化较少，并且遵从“二八原则”的数据。这里的“二八原则”指的是 80% 的数据访问会集中在 20% 的数据上。

也就是说，如果我们将这 20% 的数据缓存起来，那么这些数据就会具有非常高的读写比。读写比越高，说明缓存中的数据被使用的次数也就越多，从而节省的数据库访问也就越多，缓存的优势也就越明显。

需要特别注意的是，缓存技术并不适用于那些需要频繁修改的数据。对于这种需要频繁修改的数据来说，经常会出现刚刚写入缓存的数据还没来得及被读取就已经失效了的场景。所以，在这种情况下，缓存不仅不会带来性能提升，反而会增加系统开销。

从理论上来讲，缓存的作用是辅助提升数据的读取性能，缓存数据丢失或者缓存不可用不应该影响整个系统的可用性，因为即使没有了缓存，数据依旧可以从数据库中获得。但是，现在的数据库已经习惯了有缓存的日子，假如哪天缓存系统奔溃了，就会在短时间内有大量的请求来访问数据库，数据库就很可能会因为无法承受这样的并发压力而宕机。

为了解决这个问题，有些网站会使用缓存热备等技术手段来提供缓存的高可用性，即：当某台缓存服务器宕机的时候，会将缓存访问切换到热备的缓存服务器上。

另外，如果你采用了分布式缓存服务器集群的话，那么缓存的数据将被分布到集群中的多台服务器上，当其中一台服务器宕机的时候，也只会丢失一部分缓存数据，此时通过访问数据库来重建这些缓存数据的开销并不算太大。

目前，分布式缓存架构的主流技术方案有两种：

一种是，在企业级应用中广泛采用的 JBoss Cache。JBoss Cache 需要在缓存集群中的每台机器上同步所有缓存的副本，当集群规模比较大的时候，同步代价会很高。而且，多份副本也会造成存储资源的浪费。但其最大的优点是速度非常快，所以 JBoss Cache 更适用于企业级规模不是很大的缓存集群。这种企业级的集群一般在几台到十几台服务器的规模。

另一种是，在互联网应用的主流 Memcached。Memcached 属于互不通信的分布式架构，集群中各个节点缓存的数据都不一样，缓存使用者基于 Hash 一致性算法来定位具体的内容到底缓存在集群中的哪个节点。
因此，Memcached 具有缓存容量大，存储效率高，可以很方便地支持集群的扩展，但是速度相对较慢的特点。这些特点决定了 Memcached 非常适用于现如今的互联网产品架构，几乎成为了网站分布式缓存架构的代名词。

互联网产品架构的应用服务器集群规模一般都很大，即使小规模的应用集群也有上百台机器，规模大的话可以达到上万台，这种架构下的缓存集群规模要求也非常大。

通过上面这些些缓存的基础知识，再结合着你在平时项目中积累的相关经验，相信你已经理解了缓存的原理。那么，接下来我们再从测试人员的视角来看看，在执行测试时需要考虑到哪些与缓存相关的测试场景：

对于前端的测试场景，需要分别考虑缓存命中和缓存不命中情况下的页面加载时间。

基于缓存过期测试策略的设计，需要考虑到必须要重新获取数据的测试场景。

需要针对可能存在的缓存“脏数据”，进行有针对性的测试。缓存“脏数据”，是指数据库中的数据已经更新，但是缓存中的数据还没来得及更新的场景。

需要针对可能的缓存穿透进行必要的测试。缓存穿透，是指访问的数据并不存在，所以这部分数据永远不会有被缓存的机会，因此此类请求会一直重复访问数据库。

系统冷启动后，在缓存预热阶段的数据库访问压力是否会超过数据库实际可以承载的压力。

对于分布式缓存集群来说，由于各集群使用的缓存算法不同，那么如果要在缓存集群中增加更多节点进行扩容的话，扩容对原本已经缓存数据的影响也会不同。所以，我们需要针对缓存集群扩容的场景，进行必要的测试和性能评估。

集群

集群也是提升网站性能和并发处理能力的典型架构设计方法。

当一台服务器不足以满足日益增长的用户流量时，我们就可以考虑使用多台服务器来组成一个集群：外部请求将统一和负载均衡器打交道；负载均衡器根据不同的负载调度算法，将访问请求传递给集群中的某台服务器处理。

需要注意的是，在这种模式下，集群中的任何一台服务器宕机都不会给整个系统带来明显的影响。此时，每台服务器的地位也都不怎么高，我们可以直接替换掉出现了问题的某台服务器。同样地，当需要支持更大的系统负载时，我们就可以在集群中添加更多的服务器。

这时，集群中的每台服务器都可以被随时替换或者淘汰掉，就像“牲口”似的可以任人宰割。所以，这种模式，就有点类似于“牲口”模式。

与“牲口”模式对应的是“宠物”模式，比如一些企业级的应用，它们往往不通过集群来扩展系统的能力和提高系统的性能，而是采用更为强劲的服务器。

这种性能非常强大的单台服务器，价格往往十分昂贵，所以通常都会被特别关照，比如给其配备最好的机房和 UPS 等等。另外，大家都不敢对这样的服务器有任何大的动作，生怕把它们搞坏了。此时，这些价格昂贵的服务器更像是“宠物”。

综上所述，现今的互联网应用采用的都是“牲口”模式。在这种模式下，我们在开展测试时，相应地需要额外关注以下这些测试点：

集群容量扩展。也就是说，集群中加入新的节点后，是否会对原有的 session 产生影响。

对于无状态应用，是否可以实现灵活的实效转移。

对于基于 session 的有状态应用，需要根据不同的 session 机制验证会话是否可以正常保持，即保证同一 session 始终都有同一个确定的节点在处理。

当集群中的一个或者多个节点宕机时，对在线用户的影响是否符合设计预期。

对于无状态应用来说，系统吞吐量是否能够随着集群中节点的数量呈线性增长。

负载均衡算法的实际效果，是否符合预期。

高并发场景下，集群能够承载的最大容量。

总结

今天，我以测试人员的视角，和你分享了网站高性能架构设计中，需要重点关注的点。

首先，网站的性能，在很大程度上和用户的体量有直接关系。因此，开发人员在设计网站架构时，必须要重点考虑与性能相关的架构设计。相应地，测试人员在测试网站性能时，也要考虑到这其中的架构设计。

其次，网站高性能架构设计，主要包括前端性能优化和后端服务器的性能调优。所以，我从这两个方面，和你展开了今天的分享。测试人员在理解了两大部分知识的基础上，在设计具体的测试时，要考虑到这些网站高性能架构设计的方案、技术手段，以此制定出需要额外增加的测试点，以及对应的测试方法。

最后，关于网站性能测试的理论与方法，你可以参考我在第 28~34 篇文章（也就是性能测试系列文章）中的相关内容。

思考题

在你接触过的项目中，都遇到过哪些系统层面的高性能架构设计方案？从测试的角度来看，你又会设计怎样的测试场景和用例？

感谢你的收听，欢迎你给我留言。

50 | 深入浅出网站高可用架构设计

你好，我是茹炳晟。今天我和你分享的主题是：深入浅出网站高可用架构设计。

在今天这篇文章中，我将沿着网站架构的话题，和你继续聊聊高可用的架构设计。

顾名思义，网站高可用指的就是，在绝大多的时间里，网站一直处于可以对外提供服务的正常状态。业界通常使用有多少个“9”来衡量网站的可用性指标，具体的计算公式也很简单，就是一段时间内（比如一年）网站可用的时间占总时间的百分比。

我用下面这个表格，列出了四种最常见的可用性等级指标，以及允许的系统不可用时长。

可用性等级 | 通俗叫法 | 量化的可用性等级 | 一年中允许的不可用时长
基本可用 | 2 个 9 | 99% | 87.6 小时
较高可用 | 3 个 9 | 99.9% | 8.8 小时
具备故障自动恢复能力的可用 | 4 个 9 | 99.99% | 53 分钟
极高可用 | 5 个 9 | 99.999% | 5 分钟

一般，我们以“年”为单位来统计网站的可用性等级。“9”的个数越多，一年中允许的不可用时间就越短，当达到 5 个“9”的时候，系统全年不可用时间只有区区 5 分钟，可想而知这个指标非常难达到。

所以一般来讲，业界的网站能做到 4 个“9”，也就是说在一年内只有 53 分钟的时间网站是处于不可用状态，就已经是算是非常优秀了。

另外，可用性指标还有个特点，越往后越难提高，需要付出的经济成本和技术成本都会呈现类似指数级的增长。因此，在实际的网站架构设计过程中，到底需要做到几个“9”还需要结合具体的业务要求，以及风险评估来最终确定。

那么，接下来我就首先和你分析一下造成网站不可用的主要原因，然后再基于这些原因谈谈我们可以通过哪些对策和方法，将这些造成网站不可用的因素的影响降到最低。

其实，造成网站不可用的主要原因有以下三大类：

服务器硬件故障；

发布新应用的过程；

应用程序本身的问题。

服务器硬件故障

网站物理架构中，随机的硬件服务器的故障，比如某台服务器由于硬件故障宕机，可以说不是偶然，而是必然会发生的。尤其是目前互联网企业普遍采用的“牲口”模式集群方案。

而且随着网站规模不断扩大，网站后台的服务器数量也越来越多，所以由硬件故障引起问题的概率也是不断飙升。

所以，网站的高可用架构设计，需要保障的是即使出现了硬件故障，也要保证系统的高可用。

发布新应用的过程

网站的新版本发布过程中，往往会出现需要重新部署新的应用程序版本，然后再重启服务的情况。如果这个更新过程中不采用特殊技术手段的话，也会造成短暂的服务不可用。而且这种形式的不可用，相比服务器硬件故障的不可用更为常见。

原因很简单，互联网网站的功能更新迭代非常快，基本都是以“天”为单位来发布上线的，也就是说几乎每天都有需要中断服务来完成服务升级的可能。

显然，从业务角度来看，这种为了应用升级造成的服务不可用，完全不可能被接受。这就好比 eBay 或者淘宝告诉你说，我们每天某个时间段需要内部升级维护无法对外提供服务一样，让人无法接受。

从网站可用性指标的角度来看，这种频繁出现的停机升级过程将大大增加网站的不可用时间。因此，我们的高可用架构设计必须能够提供切实可行的方案，将这种停机升级的影响降到最小。

应用程序本身的问题

造成网站不可用的最后一个原因是，应用程序本身的问题。

比如，发布的应用程序版本身存在潜在的内存泄露，那么经过较长时间的运行积累后，最终会造成服务器的内存被占满，之后必须要靠重启服务来恢复。那么，这个时候就会引入短暂的服务不可用时间。

再比如，应用程序在测试环境没有经过充分的测试验证，或者说由于测试环境的配置和实际生产环境之间存在差异，有可能造成应用程序在生产环境部署完后无法使用的情况，从而造成服务不可用。

由此可见，应用程序在上线发布前进行充分、全面的测试，是多么的重要。无论是立竿见影就能发现的功能缺陷，还是需要长期运行才能暴露的软件问题，都可以通过软件测试去发现，然后反馈给开发人员去解决，从而避免造成系统的不可用。同时，我们也需要尽可能减少测试环境和生产环境的差异，尽可能采用完全相同的环境以及第三方依赖。

网站高可用架构设计

为了系统性地解决造成系统不可用的上述三类问题，提高网站的可用性，我们在网站高可用架构设计上，探索出了对应的三类方法。

第一类方法是，从硬件层面加入必要的冗余；

第二类方法是，灰度发布；

第三类方法是，加强应用上线前的测试，或者开启预发布验证。

对于第一类硬件故障造成的网站不可用，最直接的解决方案就是从硬件层面加入必要的冗余，同时充分发挥集群的“牲口”优势。

比如，对于应用服务器来说，即使没有伸缩性的要求，我们也会至少采用两台同样的服务器，并且引入一台额外的负载均衡器，所有的外部请求会先到负载均衡器，然后由负载均衡器根据不同的分配算法选择其中的某一台服务器来提供服务。

备注：伸缩性是指通过增加或减少服务器的数量，就可以扩大或者减小网站整体处理能力。我会在下一篇文章中和你详细分享。

这样，当其中一台服务器硬件出现问题甚至宕机的时候，另一服务器可以继续对外提供服务。这时，在外部看来系统整体依然是可用的，这就给恢复那台故障服务器提供了时间。而两台服务器同时出现硬件故障的概率是很低的。

因此，从测试人员的角度来看，知道了应用服务器集群的工作原理，就可以在设计测试的时候，针对集群中的某一个或者某几个节点的故障情况设计测试用例。

再比如，对于数据存储的服务器来说，往往通过数据冗余备份和失效转移机制来实现高可用。为了防止存储数据的服务器发生硬件故障而造成数据丢失，我们往往会引入多个数据存储服务器，并且会在数据有更新操作的时候自动同步多个数据存储服务器上的数据。

也就是说，数据的存储存在多个副本，那么当某台数据存储服务器故障的时候，我们就可以快速切换到没有故障的服务器，以此保证数据存储的高可用。

那么，从测试人员的角度来看，我们依旧可以针对这种情况设计出针对部分数据服务器发生故障时的测试用例，以完成系统应对故障的反应情况的测试。

对于第二类由于发布新应用造成的系统不可用，我们采用的主要技术手段是灰度发布。

使用灰度发布的前提是，应用服务器必须采用集群架构。假定现在有一个包含 100 个节点的集群需要升级安装新的应用版本，那么这个时候的更新过程应该是：

首先，从负载均衡器的服务器列表中删除其中的一个节点；

然后，将新版本的应用部署到这台删除的节点中并重启该服务；

重启完成后，将包含新版本应用的节点重新挂载到负载均衡服务器中，让其真正接受外部流量，并严密观察新版本应用的行为；

如果没有问题，那么将会重复以上步骤将下一个节点升级成新版本应用。如果有问题，就会回滚这个节点的上一个版本。

如此反复，直至集群中这 100 个节点全部更新为新版本应用。

在这个升级的过程中，服务对外来看一直处于正常状态，宏观上并没有出现系统不可用的情况。就好比是为正在飞行中的飞机更换引擎，而飞机始终处于“正常飞行”的状态一样。

对于第三类应用程序本身的问题造成的系统不可用，我们一方面要加强应用程序上线部署前的测试以保证应用本身的质量，另一方面需要启用所谓的预发布验证。

我们一定遇到过这样的尴尬情况：应用在测试环境中经过了完整、全面的测试，并且所有发现的缺陷也已经被修复并验证通过了，可是一旦发布到了生产环境，还是立马暴露出了很多问题。

这其中的主要原因是，测试环境和生产环境存在差异。比如，网络环境的限制可能不一样；再比如，依赖的第三方服务也可能不一样，测试环境连接的是第三方服务的沙箱环境，而生产环境连接的是真实环境。

为了避免这类由于环境差异造成的问题，我们往往会预发布服务器。预发布服务器和真实的服务所处的环境没有任何差别，连接的第三方服务也没有任何差别，唯一不同的是预发布服务器不会通过负载均衡服务器对外暴露，只有知道其 IP 地址的内部人员才可以对其进行访问。

此时，我们就可以借助自动化测试来对应用做快速的验证测试。如果测试通过，新的应用版本就会进入到之前介绍的灰度发布阶段。这种做法，可以尽最大可能保证上线应用的可用性。

总结

今天我和你分享了衡量网站高可用性的指标，对于一些大型网站来说，达到 4 个“9”（即 99.99%，一年中的不可用时间不超过 53 分钟）已经算是优秀了。

然后，我将影响网站高可用的因素归为了三类，并相应地给出了解决这三类问题的方案：

由服务器硬件故障引起的网站不可用，对应的解决方案是从硬件层面加入必要的冗余；

由发布新应用的过程引入的网站不可用，对应的解决方案是采用灰度发布的技术手段；

由应用本身质量引入的网站不可用，对应的解决方案是，一方面加强测试提高应用本身的质量，另一方面是引入预发布服务器消除测试环境和生产环境的差异。

思考题

关于高可用架构设计，我在文章中和你分享了应用服务器和数据存储服务器的高可用架构。但是，我并没有介绍缓存服务器的高可用架构。那么，你认为缓存服务器是否也需要高可用架构的支持呢？如果需要的话，缓存集群的高可用架构应当如何设计？如果不需要，也请你分享一下你的理由。

感谢你的收听，欢迎你给我留言。

51 | 深入浅出网站伸缩性架构设计

你好，我是茹炳晟，今天我分享的主题是：深入浅出网站伸缩性架构设计。

目前，很多测试工程师，甚至是开发工程师都一直分不清楚可伸缩性和可扩展性这两个概念，主要原因是从字面上看这两个概念的确有相似之处。但实际情况呢，可伸缩性和可扩展性这两两个概念的含义相差十万八千里，根本不具有任何可比性。

所以，我将通过两篇文章来和你分享，网站的伸缩性和可扩展性架构设计到底是什么，以及在设计测试用例时需要注意哪些点。

可伸缩性和可扩展性的概念区别

可伸缩性翻译自 Scalability，指的是通过简单地增加硬件配置而使服务处理能力呈线性增长的能力。最简单直观的例子，就是通过在应用服务器集群中增加更多的节点，来提高整个集群的处理能力。

而可扩展性翻译自 Extensibility，指的是网站的架构设计能够快速适应需求的变化，当需要增加新的功能实现时，对原有架构不需要做修改或者做很少的修改就能够快速满足新的业务需求。

接下来，我就先和你分享下网站的可伸缩性架构。而关于网站的可扩展性架构设计，我会在下一篇文章中和你详细分享。

分层的可伸缩性架构

网站的可伸缩性架构设计主要包含两个层面的含义：

一个是指，根据功能进行物理分离来实现伸缩；

另一个是指，物理分离后的单一功能通过增加或者减少硬件来实现伸缩。

在根据功能本身进行物理分离来实现伸缩的过程中，还有两种不同的实现方式：

一种是功能的“横切”，比如一个电商网站的购物功能从上至下就可以分为界面 UI 层、业务逻辑处理层、公共服务层和数据库层，如果我们将这些层区分开来，每个层就可以独立实现可伸缩；

图 1 功能“横切”示意图

另一种是功能的“纵切”，比如一个电商网站可以根据经营的业务范围（比如书店、生鲜、家电和日化用品等）进行功能模块的划分，划分后的每个业务模块都可以独立地根据业务流量和压力来实现最适合自己规模的伸缩性设计。

图 2 功能“纵切”示意图

同样地，对于单一功能可以通过增加或者减少硬件来实现的可伸缩性，也有两种不同的实现方式：

一种是纵向的可伸缩性，指的是通过增加单一服务器上的硬件资源来提高处理能力。比如，在现有服务器上增加 CPU、内存，或者在现有的 RAID/SAN 存储中增加硬盘等。
传统软件企业使用的“宠物”模式，就是通过这个思路来实现有限的可伸缩性，我们往往把这种方式的伸缩性称为单节点的可伸缩性。显然，在如今海量互联网流量的情况下，想仅仅依赖于某一台服务器来处理各种请求显然是不可能的。

另一种是横向的可伸缩性，指的是通过使用服务器集群来实现单一功能的可扩展性。当一台机器不足以处理大量用户并发请求的时候，我们就采用多台机器组成集群来共同负担并发压力。
这种方式是基于集群的可伸缩性实现的，也是目前最主流的网站可伸缩性方法，也就是我之前提到过的“牲口”模式。很多时候当我们谈及网站的可伸缩性设计时，如果没有特定的上下文或者特指的场景，往往指的都是基于集群的可伸缩性。

图 3 单一功能通过增加或者减少硬件来实现的可伸缩性

基于集群的可伸缩性设计，是和网站本身的分层架构设计相对应的：

在应用服务器层面有应用服务器集群的可伸缩性架构设计；

在缓存服务器层面有缓存服务器的可伸缩性架构设计；

在数据库层面有数据库服务器的可伸缩性架构设计。

虽然都是可伸缩性设计，但是由于应用服务器、缓存服务器和数据库服务器本身的架构在设计上就有所区别，加之它们的使用场景不同，使得它们的可伸缩性架构设计就有着巨大的差异。

接下来，我就先简单解释一下这三个层面的可伸缩性设计指的是什么，以及从测试人员的角度来看我们需要关注哪些点。

应用服务器的可伸缩性设计

应用服务器的可伸缩性设计是最直观，也是最容易理解的。当一台应用服务器不足以支撑业务流量的时候，我们就可以用多台服务器来分担业务流量。

但是，为了保证这批服务器对外暴露的是一个统一的节点，我们就需要一个负载均衡器作为统一的窗口来对外提供服务，同时负载均衡器会把实际的业务请求转发给集群中的机器去具体执行。

这里需要特别注意的是，负载均衡器并不是按照你在字面上理解的“均衡”那样，把业务负载平均分配到集群中的各个节点，而是通过负载均衡算法（比如轮询算法、基于加权的轮询算法、最小链接算法等）将用户流量分配到集群机器。从这个意义上说，将负载均衡器称为任务分配器才更合适。

图 4 通过负载均衡器实现的应用服务器集群示意图

为了实现线性可伸缩性，我们希望应用本身是无状态的。此时，任何请求都可以在集群中任意节点上来执行，也就是说集群的处理能力将随着节点数量的增多呈现线性增长的态势。

但是，如果应用本身是有状态的，那么就会要求基于一次会话（session）的多次请求都被分配到集群中某一台固定的服务器上去执行。

理解了上述应用服务器集群的可伸缩性架构原理后，我们再从测试人员的角度来想想，应该考虑哪些相关的测试场景。为此，我总结了以下几点供你参考：

需要通过压力测试来得出单一节点的负载承受能力；

验证系统整体的负载承受能力，是否能够随着集群中的节点数量呈现线性增长；

集群中节点的数量是否有上限；

新加入的节点是否可以提供和原来节点无差异的服务；

对于有状态的应用，是否能够实现一次会话（session）的多次请求都被分配到集群中某一台固定的服务器上；

验证负载均衡算法的准确性。

缓存集群的可伸缩性设计

缓存集群的可伸缩性设计，相比应用服务器集群要复杂得多。

传统的缓存服务器集群是无法通过简单地加入新的节点来实现扩容的，其中的根本原因，就要从缓存的核心原理开始讲起了。

假定，一个缓存集群中有 3 台机器，那么我们在将需要缓存的内容存入缓存集群的过程，包括了这三步：

首先，将需要缓存的内容的 Key 值做 Hash 运算；

然后，将得到的 Hash 值对 3 取余数；

最后，将缓存内容写入余数所代表的那台服务器。

而此时，如果我们在缓存集群中加入了一台新的机器，也就是说缓存集群中机器的数量变成了 4。这时 Key 的 Hash 值就应该对 4 取余，你会发现这么一来，原本已经缓存的绝大多数内容就都失效了，必须重构整个缓存集群。而这，显然不能被接受。

为了解决上述这个问题，使得缓存集群也可以做到按需、高效地伸缩，那就必须采用更为先进的 Hash 一致性算法。这个算法可以很巧妙地解决缓存集群的扩容问题，保证了新增机器节点的时候大部分的缓存不会失效。

如果你想了解 Hash 一致性算法更详细的细节，请自行百度。

同样地，知道了缓存集群扩容的实现细节后，我们再从测试人员的角度出发，看看需要额外关注哪些点。这里，我总结了以下几点供你参考：

针对缓存集群中新增节点的测试，验证其对原有缓存的影响是否足够小；

验证系统冷启动完成后，缓存中还没有任何数据的时候，如果此时网站负载较大，数据库是否可以承受这样的压力；

需要验证各种情况下，缓存数据和数据库数据的一致性；

验证是否已经对潜在的缓存穿透攻击进行了处理，因为如果有人刻意利用这个漏洞来发起海量请求的话，就有可能会拖垮数据库。

数据库的可伸缩性设计

从实际应用的角度来看，数据库的可伸缩性设计主要有四种方式：

第一种方式是目前最常用的业务分库，也就是从业务上将一个庞大的数据库拆分成多个不同的数据库。比如，对于电商网站来说，它们可以考虑将用户相关的表放在一个数据库中，而商品相关的表放在另一个数据库中。

这种方式本身也符合模块设计分而治之的思想，但最大的问题是跨数据库数据的 join 操作只能通过代码在内存中完成，实现代价和成本都比较高。这种方式目前在一些中大型电商有不同程度的应用。

第二种方式是读写分离的数据库设计，其中主库用于所有的写操作，从库用于所有的读操作，然后主从库会自动进行数据同步操作。这样一来，主库就可以根据写操作来优化性能，而从库就可以根据读操作来优化性能。

但是，这个架构最大的问题在于可能出现数据不一致的情况。比如，写入的数据没能及时同步到从库，就可能会出现数据不一致。另外，这种读写分离的设计对数据库可伸缩性的贡献来讲，比较有限，很难从根本上解决问题。

这种方式主要应用在中小型规模的网站中，同时读写分离的设计也通常会和业务分库的设计一起采用，来提高业务分库后的数据库性能。

为了进一步提高数据库的可伸缩性，于是就出现了第三种数据库的可伸缩性设计：分布式数据库。分布式数据库同样存在数据不一致的问题，并且，这个方法通常只在单个数据表异常庞大的时候才会被采用，否则我还是更推荐业务分库的方法。这种数据库设计可以说是比较主流的应对大规模高并发应用的数据库方案。

第四种方式则是完全颠覆了传统关系型数据数据库的 NoSQL 设计。NoSQL 放弃了事务一致性，并且天生就是为了可伸缩性而设计的，所以在可伸缩性方面具有天然优势。因此，在互联网领域被广泛使用。

从测试的角度出发，无论是数据库架构哪种设计，我们一般都会从以下几个方面来考虑测试用例的设计：

正确读取到刚写入数据的延迟时间；

在数据库架构发生改变，或者同样的架构数据库参数发生了改变时，数据库基准性能是否会发生明显的变化；

压力测试过程中，数据库服务器的各项监控指标是否符合预期；

数据库在线扩容过程中对业务的影响程度；

数据库集群中，某个节点由于硬件故障对业务的影响程度。

总结

首先，我和你分享了可伸缩性翻译自 Scalability，而可扩展性翻译自 Extensibility，从英文单词的含义上我们就可以看出这两个概念间的差异了。

在我看来，网站的可伸缩性架构设计主要包含两个层面的含义，一个是指根据功能进行物理分离来实现伸缩，另一个是指物理分离后的单一功能通过增加或者减少硬件来实现伸缩。

从整体架构的角度来看，应用服务器、缓存集群和数据库服务器各自都有适合自己的可伸缩性设计策略：应用服务器主要通过集群来实现可伸缩性，缓存集群主要通过 Hash 一致性算法来实现，数据库可以通过业务分库、读写分离、分布式数据库以及 NoSQL 来实现可伸缩性。

而相应的理解了网站的可伸缩性架构设计后，我们在开展测试时，就可以非常自信地设计出有针对性的测试用例了。

思考题

你所接触的被测系统，是否采用了可伸缩性的架构设计方案？在具体开展测试的时候，你又是如何设计测试用例的呢？

感谢你的收听，欢迎你给我留言一起讨论。

52 | 深入浅出网站可扩展性架构设计

你好，我是茹炳晟。今天我和你分享的主题是：深入浅出网站可扩展性架构设计。

在上一篇文章中，我从可伸缩性和可扩展性对应的英文术语的角度，和你分享了这两个概念的差异，并且和你详细介绍了网站的可伸缩性架构设计主要包括的内容，以及从测试人员的视角需要关注哪些点进行针对性的测试。

所以，在今天这篇文章，也是这个专栏最后一篇正文中，我会再和你详细讨论网站的可扩展性（Extensibility）架构设计。这里，我先带你一起回顾一下可扩展性的定义：

可扩展性，指的是网站的架构设计能够快速适应需求的变化，当需要增加新的功能实现时，对原有架构不需要做修改或者做很少的修改就能够快速实现新的业务需求。

从这个定义中，我们很容易就可以得出衡量网站可扩展性设计优秀与否的主要标准，就是增加新功能的时候对原有系统的影响是否足够小。

当今的商业环境决定了网站新功能开发与上线的时间周期必须非常短，如果每次添加新功能，都需要对原有系统进行大量修改，从而还会牵连出更多测试工作的话，那么你的竞争力就会被大打折扣，用一个不太恰当的比喻就是直接“输在了起跑线上”。

其实，你我都清楚添加新功能时必须要对系统进行大幅度修改的原因是，系统架构设计上的耦合性。那么，有什么“好的”架构设计方案可以使得我们添加新功能的时候，只需对原有系统做少量修改，甚至完全不需要修改吗？

咋一听起来，这就像“又要马儿跑，又要马儿不吃草”。但，其实不是的。我们往往可以通过架构上的设计优化来达到事半功倍的效果。

为了帮助你理解可扩展性，我先和你分享一个案例。

网站可扩展性架构设计的案例

假设你现在为了实时监控服务器的健康状态，需要为网站添加一个实时收集服务器端监控指标的功能，此时最直接的方案就是用代码去实现对每一个监控指标的收集，然后将所有的这些代码集成在一起形成一个可执行程序运行在服务器端后台。

这样的设计固然简单直接，而且也能实现所有的功能需求（收集各种监控指标），但是当你需要收集一个新的监控指标时，就不得不更新整个可执行程序了。如果你需要经常添加新的监控指标的话，那么这样的设计就不能满足可扩展性的要求了。

我们希望的是，当增加新的监控指标的时候，原有的系统不需要做任何修改，甚至可以做到实时添加全新的监控指标。为了达到这个目的，现有的其他方案都不能满足或者不容易满足这个要求，所以我们就必须要在架构设计上做些文章了。

我们可以把对每一个监控指标的代码实现，直接打包成一个个的可执行监控子程序，比如收集 CPU 使用率的程序 A、收集内存使用率的程序 B 等，然后运行在服务器后台的监控主程序通过调用这些子程序，比如程序 A 和 B，来实现所有的监控需求。

这时，再增加新的监控指标时，原有系统就不需要做任何改动，只需要独立实现新的监控子程序，然后以配置文件的形式“告诉”主程序新添加的监控子程序的路径即可。这也就实现了系统的可扩展性。

接下来，我们再一起回到网站的可扩展性设计上来。其实，提升网站可扩展性性的核心，就是降低系统各个模块和组件之间的耦合。耦合程度越低，各个模块和组件的复用可能性就越大，系统的可扩展性也会越好。

从现在来看，实现网站可扩展性架构的主要技术手段包括事件驱动架构和微服务架构。

微服务架构从根本上改变了网站的架构形式，带来可扩展性便利的同时，还带来了很多其他优秀的特性。在微服务架构下，一个大型复杂软件系统不再由一个单体组成，而是由一系列的微服务组成。其中每个微服务可被独立开发和部署，各个微服务之间是松耦合的。每个微服务仅专注于完成一件任务，并要很好地完成该任务。

在微服务架构下，当网站需要增加新功能时，我们除了可以添加新的业务逻辑外，还可以利用原本已经存在的微服务来构建新的功能。由于服务和服务之间是相互隔离的，并且单个服务还可以被其他多个服务复用，所以系统的可扩展性会比较好。

而关于微服务架构下，测试人员应该关注的测试点，建议你参考专栏的第 24 篇文章《紧跟时代步伐：微服务模式下 API 测试要怎么做？》中的相关内容。如果还有哪些不清楚的，你可以再自行查找更多的相关资料，或者给我留言一起讨论。

所以，在今天这篇文章中，我会和你重点分享事件驱动架构是如何提升网站的可扩展性的。

而事件驱动架构的落地靠的是消息队列，所以我会同时和你分享消息队列的内容。最后，我会再和你分享引入了消息队列后，从测试人员的角度来看会有哪些需要额外关注的点。

事件驱动架构与消息队列

事件驱动架构设计的出发点源于这样一个事实：如果系统的各个模块之间的协作不是通过直接的调用关系来实现的，那么系统的可扩展性就一定会更好。问题是，系统的各个模块间的协作如何才能不基于调用关系呢？

答案就是事件消息。系统各个模块之间只是通过消息队列来传输事件消息，而各模块之间并没有直接的调用关系、保持松散的耦合关系。

事件驱动架构最典型的一个应用就是操作系统中常见的生产者和消费者模式，将其应用到网站设计中就是分布式消息队列。

分布式消息队列同样采用了生产者和消费者模式：

消息的发送者负责将消息发布到消息队列中，也就是“生产者”；

另外，系统中会有一个或者多个消息接收者订阅消息，订阅目的是为了获取消息并进行处理，这里的消息订阅者其实就是“消费者”。消息接收者发现消息队列中有新的消息后，就会立马对其进行处理。

可以看到，在这种模式下，消息的发送者和接收者之间并没有任何直接的联系，是松耦合的。它们的协作是通过消息队列这个“中间人”进行的。消息的发送者将消息发送至消息队列后，就结束了对消息的处理，而消息的接收者只是从消息队列中获取消息进行后续的处理，并不需要知道这些消息从哪里来，因此可以很方便地实现高可扩展性。

所以，采用这种模式的话，当网站需要增加新功能的时候，只要增加对应的新模块，再由对此模块感兴趣的“消费者”进行订阅，就可以实现对原有系统功能的扩展了，而对原本的系统模块本身并没有影响。

此时，消息队列的架构如图 1 所示。

图 1 消息队列的原理图

引入了消息队列后，我们不仅可以提高系统的可扩展性，还可以再一定程度上改善网站架构的高性能、高可用性和可伸缩性。

从性能方面来看，消息发送者不需要等接收者实际处理完成后才返回，也就是从原本的同步处理变成了异步处理，所以用户会感知到网站性能的提升。

从高可用方面来看，假如消息的接收者模块发生了短时间的故障，此时并不会影响消息发送者向消息队列中发送消息，等到消息接收者模块恢复后可以继续后续的处理，只要这段时间内消息队列本身没有被塞满而出现消息丢失的情况。从整体角度看，系统并不会感知到消息接收者模块曾经发生过短暂故障，也就相当于保证了系统的高可用。

从可伸缩性方面来看，消息队列的核心其实就是一个无状态的存储。所以，当系统需要能够保留更多的消息时，我们通过简单地增加存储空间就可以实现。尤其是，大规模的电商网站来更会将消息队列扩展成为分布式消息队列集群，来实现消息队列的可伸缩性。

引入消息队列后，测试人员需要额外关注的点

现在，你应该已经掌握了消息队列的基本原理，以及在网站架构中的用法。接下来，我们再一起看看消息队列对测试的影响，以及我们在测试时需要特别关注哪些点。

这里，我把测试人员需要额外关注的点，归纳为了以下几点：

从构建测试数据的角度来看，为了以解耦的方式测试系统的各个模块，我们就需要在消息队列中构造测试数据。这也是为什么很多互联网的自动化测试框架中都会集成有消息队列写入工具的主要原因。

从测试验证的角度来看，我们不仅需要验证模块的行为，还要验证模块在消息队列中的输出是否符合预期。为此，互联网的自动化测试框架中也都会集成消息队列的读取工具。

从测试设计的角度来看，我们需要考虑消息队列满、消息队列扩容等情况下系统功能是否符合设计预期。

除此之外，我们还需要考虑，某台消息队列服务器宕机的情况下，丢失消息的可恢复性以及新的消息不会继续发往宕机的服务器等等。

总结

在今天这篇文章中，我和你分享了网站架构知识中的最后一个内容：可扩展性。

可扩展性指的是网站的架构设计能够快速适应需求的变化，当需要增加新功能时，我们只要对原有架构进行少量修改，甚至不用修改就能快速实现新的业务需求。

从技术实现上来看，消息队列是实现可扩展性的重要技术手段之一。其基本核心原理是各模块之间不存在直接的调用关系，而是使用消息队列，通过生产者和消费者模式来实现模块间的协作，从而保持模块与模块间的松耦合关系。

引入消息队列后，测试数据的创建和测试结果的验证工作，都需要通过读写消息队列来完成。同时，我们还要考虑到消息队列满、消息队列扩容，以及消息队列服务器宕机情况下的系统功能验证。这几个点，就是测试人员需要额外关注的点了。

思考题

你在实际工作中接触过哪些种类的消息队列？在测试过程中，是否遇到过和消息队列有关的缺陷呢？

感谢你的收听，欢迎你给我留言一起讨论。

测试专栏特别放送 | 浅谈全链路压测

你好，我是茹炳晟。今天我和你分享的主题是：浅谈全链路压测。

时光飞逝，从专栏上线至今，我已经和你分享了 52 篇文章和 7 篇答疑文章，和你分享了软件测试中的各个主题，希望已经帮你构建了一幅软件测试的知识全景图。

在前面的“性能测试”系列文章中，我以 LoadRunner 为例，和你分享了传统企业在实际开展企业级性能测试的实践。并且在第 32 篇文章《无实例无真相：基于 LoadRunner 实现企业级服务器端性能测试的实践（上）》中，我和你分享了这么安排的原因，并承诺在专栏结束前，通过一篇“加餐”文章，和你分享开展全链路压测的难点，以及应对方案。

现在，就是我践行承诺的时间了。

我也不太清楚，你现在具备多少全链路压测的知识。所以，我会先和你分享一些全链路压测的理论知识，然后再分享具体的难点以及解决思路，帮你加深理解，希望可以让你听得明白、学得会、用得着。

什么是全链路压测？

全链路压测，是基于真实的生产环境来模拟海量的并发用户请求和数据，对整个业务链路进行压力测试，试图找到所有潜在性能瓶颈点并持续优化的实践。

目前，一线互联网公司的大型电商平台都会不定期地开展全链路压测，比如淘宝、京东、饿了么和美团这些企业，基本都已经有了自己的全链路压测方案和平台。

其中，最为典型的要数淘宝的双 11 活动了。每年到了 11 月 11 日的零点，淘宝的整个系统都会面临极大的流量冲击，如果事先没有经过充分的测试和容量预估，很可能会在流量爆发时瘫痪。

记得在早些年的淘宝双 11 大促中，就出现了不同程度的网站故障，严重影响了用户体验，所以从 2013 年开始，淘宝开始实施全面的全链路压测。由于在真正的双 11 到来前，淘宝内部已经模拟了比双 11 流量还要高的负载，并且逐个解决了已经发现的问题，因此真正双 11 到来的时候，就不会出现严重的问题了。

因此，为了防止此类事故，淘宝会在每在双 11 之前，就对系统的稳定性以及负载承受能力进行必要的测试和评估。

当然，全链路压测的应用场景，不仅仅包括验证系统在峰值期间的稳定性，还会包含新系统上线后的性能瓶颈定位以及站点容量的精准规划。

比如，由于某些业务模块的操作负载会集中到几个最核心的组件上，那么通过全链路压测的模拟，我们就能快速识别出哪些模块的负载过大，哪些模块的负载偏小。这样我们在对系统进行扩容时，就可以把资源更多地给到那些承受大负载的模块，而那些承受负载偏小的模块就可以进行适当的收缩来让出更多的可用资源。这，就是精准的容量规划。

单系统的独立压测

早先的时候，压测并不是针对业务的全链路来开展的，而是采用了“各个击破”的原则，即对生产环境中的单机或者单系统进行独立的压测。这时，压测主要是通过模拟单一系统的海量并发请求来实现的。而模拟海量请求主要有两种实现方式：

一种是，根据设计的压力来直接模拟大量的并发调用；

另一种是，先获取线上真实的流量请求，然后经过数据清洗后，再回放模拟大量的并发调用。

不管采用的是哪种方式，都会涉及流量模拟、数据准备、数据隔离等操作。除此之外，单系统的独立压测局限性也非常明显。

这里，我把单系统独立压测的局限性，归纳为了以下几点：

单系统压测的时候，会假设其依赖的所有系统能力都是无限的，而实际情况一定不是这样，这就造成了单系统压测的数据普遍比较乐观的情况；

在大压力环境下，各系统间的相互调用会成为系统瓶颈，但这在单系统压测的时候根本无法体现；

大压力环境下，各系统还会出现抢占系统资源（比如网络带宽、文件句柄）的情况，这种资源抢占必然会引入性能问题，但是这类问题在单系统压测过程中也无法体现出来；

由于是单系统测试，所以通常都只会先选择最核心的系统来测试，这就意味着其他的非核心系统会被忽略，而在实际项目中，这些非核心系统也很有可能会造成性能瓶颈。

因此，为了解决单系统独立压测的一系列问题，业界就衍生出了全链路压测。全链路压测会把整个系统看作一个整体，然后在真实的生产环境上尽可能真实地去模拟业务的海量并发操作，以此来衡量系统的实际承载能力，或者找出系统可能的瓶颈点并给出相应的解决方案。

目前来看，全链路压测需要解决的技术难点有很多，这里我会和你讨论其中最重要的四个点：

海量并发请求的发起；

全链路压测流量的隔离；

实际业务负载的模拟；

测试完成后的数据清理。

海量并发请求的发起

由于全链路压测需要发起的海量并发，通常会超过每秒 1000 万次以上请求的压力量级，所以传统的性能测试工具 LoadRunner 已经很难满足要求了，原因有二：

一来，LoadRunner 按并发用户数收费，这就使得采用 LoadRunner 进行互联网的全链路压测的费用会异常高；

二来，LoadRunner 本身也很难支持千万级乃至亿级的海量并发。

所以，业界基本都是采用免费的 JMeter 来完成全链路压测，这也是 JMeter 近几年被互联网企业广泛使用的原因。

但是，即便有了 JMeter，我们在开展全链路压测时，也会有很多问题需要解决。其中，最主要的问题包括以下三个：

虽然采用了分布式的 JMeter 方案，并发数量也会存在上限，比如面对亿级的海量并发时，主要原因是分布式的 JMeter 方案中，Master 节点会成为整个压测发起的瓶颈。
为了解决这个难题，很多公司并不会直接采用分布式 JMeter 架构来完成海量并发，而是会使用 Jenkins Job 单独调用 JMeter 节点来控制和发起测试压力。这样就避免了 Master 节点引发的瓶颈问题。而且，由于各个 JMeter 是完全独立的，所以只要 Jenkins Job 足够多，并且网络带宽不会成为瓶颈的情况下，就能发起足够大的并发。

测试脚本、测试数据和测试结果在分布式 JMeter 环境中的分发难题。如果直接采用分布式的 JMeter 方案，测试脚本需要通过 JMeter 的 Master 节点来分发，测试数据文件则要用户自行上传至每套虚拟机，同时测试结果还要通过 JMeter 的 Slave 节点回传至 Master 节点。
所以，更好的做法是基于 JMeter 来搭建一个压测框架，诸如脚本分发、数据分发以及结果回传等工作，都由压测框架完成。这也是目前绝大多数大型互联网企业的做法。比如，饿了么就采用这种方式搭建了压测平台，并且取得了很好的效果。

流量发起的地域要求。全链路压测流量的发起很多时候是有地理位置要求的，比如 30% 的压力负载来自上海、30% 的压力负载来自北京等，这就要求我们在多个城市的数据中心都搭建 JMeter Slave，以便可以发起来自多个地域的组合流量。

全链路压测流量和数据的隔离

因为全链路压测是在实际的生产环境中执行的，所以测试产生的数据与真实的用户数据必须进行有效隔离，以防止压测的流量和数据污染、干扰生产环境的情况。比如，不能将压测数据记录到统计分析报表里；再比如，压测完成后可以方便地清洗掉压测产生的数据。

为了达到这个目的，我们就需要对压测流量进行特殊的数据标记，以区别于真实的流量和数据。这就要求各个链路上的系统，都能传递和处理这种特殊的数据标记，同时写入数据库中的数据也必须带有这种类型的标记以便区分数据，或者直接采用专门的影子数据库来存储压测的数据。

可以看出，为了实现压测产生的和真实的流量和数据隔离，我们就需要对各个业务模块和中间件进行特殊的改造和扩展。而这个工作量相当大，而且牵涉的范围也非常广，也就进一步增加了实施全链路压测的难度。

而且通常来讲，首次全链路压测的准备周期会需要半年以上的时间，这其中最大的工作量在于对现有业务系统和中间件的改造，来实现压测流量和数据的隔离。所以，在实际的工程项目中，如果全链路压测不是由高层领导直接牵头推动的话，很难推进。

另外，在对各个业务模块和中间件添加特殊标记的改造过程中，我们会尽可能少地改动业务模块，而是更倾向于通过中间件来尽可能多地完成特殊数据标记的处理和传递。

实际业务负载的模拟

一直以来，如何尽可能准确地模拟业务系统的负载，都是设计全链路压测时的难题。这里的难点主要体现在两个方面：首先，要估算负载的总体量级；其次，需要详细了解总负载中各个操作的占比情况以及执行频次。

业界通常采用的策略是，采用已有的历史负载作为基准数据，然后在此基础上进行适当调整。具体到执行层面，通常的做法是，录制已有的实际用户负载，然后在此基础上做以下两部分修改：

录制数据的清洗，将录制得到的真实数据统一替换成为压测准备的数据，比如，需要将录制得到的真实用户替换成专门为压测准备的测试用户等等·；

基于用户模型的估算，在全链路压测过程中，按比例放大录制脚本的负载。

最后，再用这个负载来模拟全链路压测的负载。

真实交易和支付的撤销以及数据清理

由于全链路压测是在真实的生产环境中进行的，那么完成的所有交易以及相关的支付都是真实有效的，所以我们就需要在测试结束后，将这些交易撤销。

因为，我们已经对这些交易的流量和数据进行了特定标记，所以我们可以比较方便地筛选出需要撤销的交易，然后通过自动化脚本的方式来完成批量的数据清理工作。

除了上面的四大问题以外，全链路压测还需要考虑测试执行过程中的性能监控、高强度压测负载下的测试熔断机制、全链路压测执行期间对原有系统正常负载的影响、全链路压测数据对外的不可见等等。

所以说，全链路压测的技术含量很高，而且需要多方共同配合才有可能顺利完成。所以，今天这篇文章的目的，意在抛砖引玉。希望你可以借由这篇文章，先对全链路压测的难点以及对应的解决思路有个全局的认识。而如果你想要更好地了解并掌握全链路压测，最好的方式还是要在实际项目中多加历练。

另外，你还可以参考一些网上的优秀资源，我在这里列出了两条供你参考：

https://mp.weixin.qq.com/s?__biz=MzIzMzk2NDQyMw==&mid=2247486703&idx=1&sn=0c448c40469e6a8f32476a7c7fbab6cd&source=41#wechat_redirect

https://www.cnblogs.com/imyalost/p/8439910.html

总结

今天这篇文章，我和你分享了全链路压测的基本知识，以及在开展全链路压测的难点、对应的解决思路。现在，我再和你一起回顾下。

全链路压测，是基于真实的生产环境来模拟海量并发用户请求和数据，对整个业务链路进行压力测试，试图找到所有潜在性能瓶颈点并持续优化的实践。它的应用领域不仅仅包含验证系统在峰值期间的稳定性，还会包含新系统上线后的性能瓶颈定位以及站点容量的精准规划。

了解了全链路的基本概念，以及适用场景后，我和你分享了全链路压测中最关键的四个技术难点，即：海量并发请求的发起、全链路压测流量和数据的隔离、实际业务负载的模拟，以及测试完成后的数据清理。

海量并发请求的发起主要借助于 JMeter，并且通过 Jenkins Job 来实现海量并发的调度控制；

全链路压测流量和数据的隔离主要借助含有特定标记的流量和数据来实现，同时需要对业务模块以及中间件进行必要的改造，数据库这边还会使用影子数据库；

实际业务负载的模拟，主要是采用基于历史流量修改后的回放来实现；

全链路压测完成后的数据清洗，则是借助自动化的手段来批量完成。

思考题

你能想到全链路压测中还有哪些技术上的难点吗？

感谢你的收听，欢迎你给我留言一起讨论。

测试专栏特别放送 | 答疑解惑第一期

你好，我是茹炳晟。

首先，感谢大家对《软件测试 52 讲》专栏的支持与参与。

到目前为止，我已经通过测试基础知识、GUI 自动化测试、API 自动化测试、代码级测试、性能测试、测试数据准备、测试基础架构、测试新技术 8 个系列、47 篇文章，和你分享了软件测试相关的所有知识点。

每篇文章后面，我都为你留下了 1~2 个思考题。其中，一部分思考题是让你分享你所在项目和团队的实践，和其他读者一起探讨、交流，这样大家可以互相借鉴好的做法；还有一部分思考题，是针对当篇文章中的内容，希望你可以分享一些你的想法，也想借此了解你在实际的测试工作中遇到的问题，尽我的能力再多为你提供些帮助。

47 篇文章的写作，基本上已经占满了我所有的个人时间。1000 多条留言，我也没有精力去一一答复，这其中还有很多不是一两句就能解释清楚的问题。还有些留言质量非常高，分享了一些我未覆盖到的内容，观点都非常赞。

所以，特别选在专栏即将结束的节点，也可以说是和你分享完了软件测试的基本概念、原理、方法的节点上，我和编辑一起策划了这个“答疑解惑”系列，从已发布的文章，以及对应的留言中，精选出一些问题，为你解答。

当然了，我的专栏还有 5 篇正文没有更新，我已经根据大家的反馈，重新调整了这 5 篇文章的主题，选择了大家最关注的、对大家更有帮助的五个技术点，和你展开分享。下周，我将继续为你更新这些文章，敬请期待。

今天这篇文章，我就先挑选了五个问题，和你分享一下我的看法。你也可以继续在留言区给我留言，说出你的见解，我们继续讨论。

问题一：从拓展思维的角度，还可以为“用户登录”功能补充哪些测试用例？

在专栏第一篇文章《你真的懂测试吗？从“用户登录”测试谈起》中，我从“用户登录”功能的测试用例设计谈起，和你分享了一个看似简单的功能，在设计测试用例时需要考虑的方方面面。在文后，我希望你可以从拓展思维的角度，思考一下还可以为这个“用户登录”功能补充哪些测试用例。

大家针对这个问题的留言质量都非常高，考虑的也都非常全面，可以说涵盖了很多我在文章中并没有涉及到的点。

你也许已经发现了，针对“用户登录”这个简单直接的功能，我们居然可以设计出这么多的测试用例。这些测试用例，既覆盖了明确定义的软件功能性需要，也覆盖了软件非功能性需求的方方面面。

可是，当在实际项目的实施过程中，当老板让你预估一下测试一个“用户登录”功能需要多少时间和工作量的时候，如果完全按照文章中的介绍的测试用例设计方法来执行测试的话，你可能会告诉老板说：“我需要至少 2-3 天”。此时，你的老板一定会火冒三丈，怀疑你是不是在忽悠他，就这么个简单明了的功能居然要花这么长时间的测试。

因此，这么全面的测试设计和执行通常不会出现在实际的工程项目中。站在工程实践的角度，我推荐的是“够用就好”的原则。

其实，测试的目的并不是要求软件中不存在任何错误，而是希望软件在上线运行的过程中，错误不会被触发。这就要求我们在测试用例选择的过程中，要尽可能覆盖用户最常使用的操作场景进行重点测试，同时也应该基于风险的大小来决定哪些测试应该纳入当前的测试范围。这里的风险指的是，万一软件缺陷被触发将会对业务造成哪些影响。

问题二：你有哪些好的测试用例设计实践和方法？

在专栏的第二篇《如何设计一个“好的”测试用例》文章中，我和你分享了一个“好的”的测试用例应该具备的特征、常用的几种测试用例设计方法，以及三个独家“秘籍”。相应地，我也希望你可以在留言区分享一些好的实践和方法。

大家在留言区的评论很活跃，也很精彩。这里，我再和你说说我的一些实践和方法吧。

我在文章中分享的方法都是从技术层面展开的，其实还有很多流程上的方法可以帮助测试用例的设计。比如，一些资深的测试工程师，可以将一些典型的测试用例设计整理成检查点列表（Checklist）。这样，当你完成测试用例设计后，可以再基于检查点列表来自查你的用例设计是否还存在不完备的地方。

通常情况下，测试用例的设计需要通过文档来体现。所以，你往往会花费很多的时间在文档写作上，而不能将有限的精力用在“刀刃”上。为了减少设计测试用例的文档写作时间，二把更多的时间花在测试用例的设计上，我建议你引入一些速记工具，或者是“头脑风暴”类的工具，来简化测试用例设计的文档工作。

“小黄鸭”方法不仅适用于程序的调试，同样适用于测试用例设计的自我检查。“小黄鸭”方法原本的含义是，当你在调试程序的过程中，因为无法顺利找到问题源头而无法继续的时候，可以假想有一个小黄鸭是你的听众，然后你将程序实现的思路以及业务逻辑处理的细节一一讲给它听。这样在讲述的过程中，会引发你全面的思考，并可以帮助你自发地发现问题。同样地，你也可以将测试用例的设计思路讲给小黄鸭听，以此来帮助你理清思路。

问题三：你在实施单元测试时，遇到了哪些问题，又是如何解决的？

在专栏的第三篇文章《什么是单元测试？如何做好单元测试？》中，我和你分享了单元测试的概念，和你重点讨论了用例的组成，以及在实际项目中开展单元测试的方法。在文后的思考题中，我希望你可以分析一下你所在公司在实施单元测试时遇到了的问题，以及对应的解决方案。

在留言区，我也看到了一些读者的见解，也很受益。这里，我首先会分享一下 eBay 在开展单元测试时的实践；然后，再挑选一位回答得很精彩的用户，做下点评。

eBay 是全球知名的大型电子商务网站，已经大规模使用了微服务架构。对于单元测试而言，eBay 并不会对所有的微服务以及所有的中间件全面开展单元测试。我们只会选取一些偏底层的核心应用来全面开展单元测试，而对于产品的前端代码、偏业务应用的代码，很少会执行完整意义上的单元测试。

如果你在项目中推行过单元测试，并且要求能够提供代码级的覆盖统计数据的话，那么你在实施和推行单元测试的过程中，一定会遇到诸如单元测试代码覆盖率低下的问题。这里，我的建议是，在项目的早期不要硬性规定绝度百分比的代码覆盖率指标，而是建立一个覆盖率基线，然后保证代码在每次发布前，都可以与这个基线持平或者高于这个基线的覆盖率。

另外，我还在留言区看到了一个非常具有代表性的留言，也就是这个叫作“小志”的读者留言。

他很好地归纳了当今互联网企业中单元测试的现状。的确，很多的互联网公司都在追求快速的功能实现，却一直没有强调单元测试，并且由于灰度发布机制的支持，所以对软件质量隐患的容忍度比较高。这点和传统软件测试的观念差别比较大，因为传统软件测试没有灰度发布的概念，所以对每个即将发布版本的质量控制都会非常严格。

问题四：你所在项目开展的自动化测试，真的有必要吗？

在第四篇文章《为什么要做自动化测试？什么样的项目适合做自动化测试？》中，我和你分享了自动化测试的概念，以及具备哪些特征的项目才适合做自动化测试。我还希望你能理解自动化测试是一把“双刃剑”，必要一味地为了自动化而自动化。

所以，在文后我希望你根据自己所经历的项目中的自动化测试所占比重，去进一步思考这个项目开展自动化测试的必要性。

这里，我先来分享一下 eBay 在自动化测试方面的具体实践。eBay 早期是非常重视 GUI 自动化测试的，所以投入了大量的资源去大规模开发 GUI 测试用例。可是，随着用例规模的不断扩大，GUI 测试的稳定性问题也被不断放大。同时，大量 GUI 测试用例的并发执行时间也成了个大问题。

为了缓解这种状态，我们逐渐将测试重心迁移到了后端服务的 API 测试上。API 测试具有代码形式统一、执行稳定性高的特点。而原本 GUI 测试，我们降低了自动化测试用例的比例，并由探索式测试来完成部分 GUI 测试。

另外，在留言区，我看到了一些比较赞的观点。比如，这位“关柱鹏”的留言，就是站在了全局的视角描述了可重用的测试架构。这样的架构设计将非常有利于自动化测试脚本的应用。你可以参考。

问题五：你所在公司，开展代码级测试时用到了哪些自动化测试技术？

在专栏的第 5 篇文章《你知道软件开发各阶段都有哪些自动化测试技术吗？》中，我和你分享了在软件的全生命周期中，涉及到的自动化自动测试技术。我从单元测试、代码级集成测试、Web Service 测试，以及 GUI 测试阶段的自动化技术这四个方面，和你分享了“自动化测试”的技术内涵和外延。

我希望你读完这篇文章后，可以回忆或者归纳下：你所在公司，开展代码级测试时用到的自动化测试技术。

这里，我还是先和你分享一下 eBay 的实践经验。可以说，eBay 非常重视自动化测试开发，并在这方面投入了很多成本，几乎在软件研发生命周期的各个阶段都有自动化测试的支持。

比如，在单元测试阶段，有基于参数分析的自动化测试数据准备系统；在 API 测试阶段，有基于 REST Assured 的 API 自动化测试；在系统测试或者 E2E 测试中，用到了 GUI 自动化框架设计与开发。可以说，自动化测试已经渗入到了 eBay 软件研发的各个领域。

在留言区中，“康美之心 淇水之情”的留言，在我看来是一个能够很好的落地实践。Spring-boot、REST Assured、Cucumber 都是当下最热门的技术。由于使用了 Cucumber，所以完全有可能可以实现自己特有的基于 API 的行为驱动开发（BDD）。

一方面 Cucumber 本身就是非常成熟的 BDD 框架，另一方面 REST Assured 也支持三段式的测试用例描述。所以，用 Cucumber 结合 REST Assured 的方案，可以实现更灵活的 API 级别的 BDD，用 Cucumber 结合 Selenium 可以实现 GUI 级别的 BDD。

最后，感谢你能认真阅读这五篇文章的内容，并留下你的学习痕迹。相信这些留言，于你于我而言，都是一笔宝贵的财富。期待你能继续关注我的专栏，继续留下你对文章内容的思考，我也在一直关注着你的留言（虽然有时未能及时回复）。

测试专栏特别放送 | 答疑解惑第七期

你好，我是茹炳晟。

今天的“答疑解惑”系列文章，我们一起来解决测试新技术、测试人员的互联网架构核心知识这最后两个系列相关的问题。

这期的答疑文章，我不会针对每篇文章后面的思考题展开，而是会选择了四个大家比较关注的问题，和你分享我的观点。如果你的看法不同，或者你还有哪些其他问题的话，欢迎你在这篇文章下面给我留言，我会持续不断地解答你的问题。

当然了，我还是会先用一句话简单概括下每篇文章的内容，并给出对应的链接，方便你复习。

测试新技术系列文章回顾

在专栏的第 43 篇文章《发挥人的潜能：探索式测试》中，我和你阐述了这样一个基本思想：探索式测试是一种软件测试风格，而不是一种具体的软件测试技术。

作为一种思维方法，探索式测试强调依据当前语境与上下文选择最适合的测试技术，并强调独立测试工程师的个人自由和责任，其目的是为了持续优化其工作的价值。

看到有用户在留言中说到想在实际项目中开展探索式测试，这里我想再给个建议：

高效开展探索式测试的前提是，对被测系统的设计以及行业应用有非常清晰的认识，同时在此基础上以发散的方式对系统可能存在的缺陷进行探索。所以，这就要求测试人员不仅要具有很深的业务领域知识，还需要很强的逻辑推理和分析能力。而这样的人才，属于比较稀缺的。

另外，探索式测试不要到了项目后期再集中展开，而是应该在各个模块级别就尽可能多地去探索，尽量在测试早期就能发现问题。

需要注意的是，一定不要在执行层面，将探索式测试变成了随机测试，你设计的所有后续测试步骤都必须是在你之前的步骤上推演出来的。而且，在执行探索性测试的过程中，你需要明确每个操作的目的是什么，是想证实自己的推论还是要推翻自己的假设。对此，你一定要做到心中有数，否则很容易就会变成无明确目的随机测试。

而从管理层的角度来看，千万不要以探索式测试发现的缺陷数量来考核团队的绩效。因为这样不仅不能提升测试效率，反而会把大量的时间浪费在一些非核心功能的测试上。

在专栏的第 44 篇文章《测试先行：测试驱动开发（TDD）》中，我和你分享了 TDD 的核心思想是：在开发人员实现功能代码前，先设计好测试用例，编写测试代码，然后再针对新增的测试代码来编写产品的功能代码，最终目的是让新增的测试代码能够通过。

正如“叶夏立”在留言中所说，TDD 如何落地才是最核心的问题。所以，我会将这个问题，作为今天这篇文章要回答的第一个问题，和你分享些我的经验。

在专栏的第 45 篇文章《打蛇打七寸：精准测试》中，我通过分析传统软件测试的短板，和你分享了精准测试的概念、必要性、核心思想，以及具体的测试方法。

因为这种测试理念，国内外成功落地的案例非常少，而这个理论也是由星云测试公司提出的，所以我借鉴了《星云精准测试白皮书》中的内容，并从我的视角为你解读了其中的部分内容。如果其中有哪些不清楚的问题，我们可以一起探讨，共同进步。

在专栏的第 46 篇文章《安全第一：渗透测试》中，我分享了渗透测试是由专业的安全专家来模拟黑客对系统发起攻击，找到并修复系统的安全漏洞，从而让真正的黑客无机可乘。在这其中，我和你详细分享了渗透测试的知识点，包括常用的测试方法、步骤、工具。

这篇文章更新后，有的用户反馈希望看到实例的演示，这样可以更生动、易于理解，所以这里我决定在今天的第二个问题中，和你分享一个实际的渗透测试实例，满足你的需求。

在专栏的第 47 篇文章《用机器设计测试用例：基于模型的测试》中，我分享了基于模型的测试（MBT）是一种基于被测系统的模型，由工具自动生成测试用例的软件测试技术。这也就决定了，相对于传统软件测试技术来说有优优劣。

所以，我们需要综合考虑项目本身的特点和人员的技术水平，以此决定是否有必要开展 MBT。关于如何判断你的项目是否适合开展 MBT，我决定作为今天的第三个问题，和你展开分享。

测试人员的互联网架构核心知识系列文章回顾

在 48 篇文章《优秀的测试工程师为什么要懂大型网站的架构设计？》中，我主要和你分享了测试人员学习网站架构知识的 why、what、how 的问题，并提出了“由广度到深度”和“自上而下”的架构学习思路，希望可以增强你学习网站架构的信心。

在第 49 篇文章《深入浅出网站高性能架构设计》中，我从测试人员的视角，和你分享了网站的高性能架构设计包括哪些部分，以及在设计测试用例时，需要着重考虑哪些点。而设计到具体的测试方法、工具问题，你可以再回顾一下第 28~34 篇文章（也就是性能测试系列文章）中的相关内容。

在第 50 篇文章《深入浅出网站高可用架构设计》中，我将影响网站高可用的因素归为了三类（即：服务器硬件故障、新应用的发布、应用程序本身的问题），并相应地给出了解决这三类问题的方案。希望这些内容可以帮到你。

在第 51 篇文章《深入浅出网站伸缩性架构设计》中，我和你分享了一个网站的可伸缩性架构设计主要包含的两个层面。其中，一个是指根据功能进行物理分离来实现伸缩，另一个是指物理分离后的单一功能通过增加或者减少硬件来实现伸缩。

在第 52 篇文章《深入浅出网站可扩展性架构设计》中，我和你分享了本专栏的最后一个主题，即网站的可扩展性架构设计。从已有的实现方案来看，实现网站可扩展性架构的主要技术手段包括事件驱动架构和微服务架构。

而在微服务的实现方案中，需要测试人员关注的点，你可以参考第 24 篇文章《紧跟时代步伐：微服务模式下 API 测试要怎么做？》中的相关内容。所以，在这篇文章中，我和你重点分享的是事件驱动架构实现的大致原理，以及测试人员需要额外关注的点。

因为这个系列的文章，更新日期比较近，所以很多用户还没来得及看。所以，我就没有在这篇答疑文章中，设置与这个系列有关的问题。如果你阅读完这个系列的文章，有任何困惑，都可以给我留言，我将和你一起讨论、解决。

问题一：什么样的项目适合 TDD？TDD 如何才能落地？

的确，TDD 这个概念从提出来到现在已经有很长时间了，但实际落地的项目并不多，甚至可以说少之又少。造成 TDD 落地困难的原因有很多，比如很多大型项目本身就不适合做 TDD，TDD 初期阶段的工作划分以及粒度控制都是难点。但我认为最重要的原因是，TDD 需要大幅改变研发团队的流程规范。这种改变在公司层面，尤其是中大型公司是很难实际执行的。

虽然明知落地 TDD 困难重重，但是你又特别想在自己的项目中尝试 TDD，以解决现在的测试方法不能解决的问题。那么，落地 TDD 有哪些可值得借鉴的经验呢？这也正是很多用户关心的，比如昵称为“叶夏立”的用户在文章下面的留言。

这里，我根据自己的时间，为你总结了如下几点：

只在一些小型项目，比如前期的 POC 项目中，尝试开展 TDD；

一定要借助 Cucumber 之类的 TDD 或者 BDD 工具，来协助 TDD 的开展；

必须把控好每个测试用例的粒度，不能太大，也不能太小，需要与开发函数以及功能的粒度相匹配；

项目管理的流程必须去适应 TDD 的实践，原本管理的是需求，现在管理的可能是测试用例了，因为用例本身就是对需求的解读；

测试人员必须要有开发背景，否则 TDD 只能是空谈；

必须要得到管理层的大力支持，最好是能自顶向下的推广。

问题二：渗透测试在落地的时候，需要注意哪些问题？

首先说一下，我设立这个问题的初衷，是想通过一个实际的例子，来帮助你理解渗透测试的本质。

所以，我以最常见的 SQL 注入攻击为例，和你简单分享下渗透测试落地时需要主要的问题。假设，我现在要测试用户登录功能，用户登录时会在界面上分别输入用户名（userName）和密码（passWord），然后程序代码会将输入的 userName 和 passWord 填充到下面 SQL 语句中。

SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"

假设，我们输入的用户名是“Robin”，密码是“12345678”。那么，此时的 SQL 语句如下所示：

SELECT * FROM users WHERE (name = 'Robin') and (pw = '12345678');"

然后，系统就会使用这个 SQL 语句去数据库中查询是否存在该用户，以及该用户的密码是否正确。

此时，如果你是黑客希望通过渗透来非法获取系统信息，你就会尝试设计以下的用户名和密码：

username 输入 "1' OR '1'='1";

password 输入 "1' OR '1'='1";

这种情况下，用于数据库查询的 SQL 语句就会变成如下所示的样子，就是将 userName 和 password 的部分用 "1’ OR ‘1’='1"都代替：

SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');"

如果你熟悉 SQL 语句的语法，你就会发现黑客查询数据库使用的 SQL 语句，其实和下面这个 SQL 语句是等价的：

SELECT * FROM users;

也就是说，原本用于查询单条用户信息的 SQL 语句已经被黑客改造成了获取全部用户信息的 SQL 语句。这，就是最典型的 SQL 注入攻击手段了。

而我们所讲的渗透测试，就是会去人为模拟这种攻击，以判断系统是否能够成功应对此类攻击。

问题三：如何判断你的项目是否适合采用 MBT，以及你认为会遇到哪些问题可能会阻碍 MBT 的开展呢？

一般来讲，只要系统的设计可以用状态转移图来描述的话，基本都可以采用 MBT。另外，基于 GUI 的系统，因为本身就可以画出页面之间相互跳转的关系图，所以也适合采用 MBT。

很可惜，eBay 内部的项目除了一些实验性的尝试，并没有大规模开展 MBT。但据我所知，业界最近有一家初创企业 AutoTest 正在全力推进 MBT 的落地和应用，而且还发表了很多相关文章，如果你对此感兴趣可以去关注一下。

在我看来，阻碍 MBT 落地的最关键问题，有两个：

一个是，探索路径的有效性问题。早期的实施方案，完全基于图论来覆盖可能路径，造成了大量的非法或者不合理的路径。但是，近几年来由于人工智能的介入，大大提升了路径探索的有效性。

另一个是，如果只是用 MBT 完成单纯测试的话，收益比会比较低。只有将 MBT 和自动化测试结合在一起，才能发挥 MBT 的优势。在这方面，eBay 一直在尝试，试图将 Selenium 和 MBT 集成到一起，目前已经有了初步成果，实现了用模型导出实际可以执行的自动化测试用例的 POC。

问题四：测试工程师如何应对面试？

首先，我并不鼓励为了应对面试，而去做特别的准备，你还是应该在平常的工作中多积累。面试的过程，本来就是尽可能地反映你真实的技术水平以及业务熟练程度的交流，关注的重点应该是如何将你自己掌握的技术和知识更好地展现出来，而不是把你不懂的知识包装成你已经“懂”的知识。为此，我有如下几点小建议供你参考：

当被问及相关测试工具的时候，除非问到了该工具使用上的细节，否则尽可能避免谈及工具使用的细节，而是应该更多地阐述工具本身的原理、和同类工具相比的优劣势，以及这个工具可以以什么样的方式帮你解决问题。这时，你的视野一定要高，不能局限于细节。当然，这也就要求你能够充分理解这个工具的原理和使用方法。

当被问及特定算法实现的时候，刚开始的时候，一定不要试图去寻求最优解法，而是要考虑最基本的实现，然后在此基础上迭代优化。因为，优秀的面试官希望看到的不是最优解，而是你解决问题的过程，以及在这个迭代过程中的逻辑推理。

当被问及你所不熟悉的测试技术时，如实回答就好，不要试图去掩饰。很多时候面试官看重的并不是你知不知道，而是当你不知道的时候你会怎么做。

最后，感谢你能认真阅读第 43~52 这 10 篇文章的内容，并写下了你的想法和问题。期待你能继续关注我的专栏，继续留下你对文章内容的思考，我也在一直关注着你的留言、你的学习情况。

咱们的答疑环节暂告一段落了，但这并不意味着结束。如果你在学习过程中遇到了什么问题，还可以继续给我留言，我会持续不断地回答你的问题。

测试专栏特别放送 | 答疑解惑第三期

你好，我是茹炳晟。

今天这篇文章是“答疑解惑”系列文章的第三期，在这一期里面我做了个小小的改变，不再是针对每一篇文章后面的思考题，以及“你”的留言，来展开分享。这次，我选择将这个专栏的第二个系列：GUI 自动化测试系列，作为一个整体，回答下你的问题。

根据目前 GUI 测试技术的应用情况，以及“你”在留言中留下的期望进一步了解的问题，也为了能够讲清、讲透，我还是选择了五个问题，为你解答。当然了，如果你关注的问题并未涵盖其中的话，也不要有遗憾，你可以在这篇文章下继续给我留言，我会持续关注并回答你的问题。

接下来，我还是会先为你总结一下，GUI 自动化测试系列的全部 10 篇文章的内容，并为你提供文章链接，希望你可以通过回顾旧文获得新知。

系列文章内容回顾

在专栏的 12 篇文章《从 0 到 1：你的第一个 GUI 自动化测试》中，我基于 Selenium 2.0，带你从 0 到 1 建立了一个最简单直接的 GUI 自动化测试用例。通过这篇文章我希望传达给你的内容是，基于 Selenium 搭建 GUI 自动化测试用例的方法，以及 Selenium 1.0、2.0 的实现原理。所以，对于具体的用例设计方面的细节，比如构建一个 GUI 测试用例时需要封装哪些类等，我并没有详细展开。

也正如“Cynthia”在留言中所说的，一些资料会直接带你分析源码，但不会为你讲述工具的原理，所以虽然你会用这个工具，但是类似于为啥 Chrome 可以跑的 case、Firefox 跑不了、为啥 Web Driver 还要一个浏览器装一个这种问题，你永远不会明白。反馈到具体的测试工作上，就会出现知道要这么做，但是不知道为什么要这么做的尴尬局面。

在专栏的第 13 篇文章《效率为王：脚本与数据的解耦 + Page Object 模型》中，我和你分享了什么是数据驱动的测试，让你明白了“测试脚本和数据解耦”的实现方式以及应用场景；然后，我从 GUI 自动化测试历史发展演变的角度，引出了 GUI 测试中的“页面对象模型”的概念。

在这篇文章最后，我希望你思考一下“是否应该在页面对象模型中封装控件的操作”这个问题。我看到留言区的回复，也都很精彩，可谓是思维清晰、逻辑严谨。这里，我想再阐述一下我的观点，详情请见问题二。

在专栏的第 14 篇文章《更接近业务的抽象：让自动化测试脚本更好地描述业务》中，我以“如何把控操作函数的粒度”和“如何衔接两个操作函数之间的页面”这两个问题为引子，和你分享了业务流程的概念、核心思想和适用的场景。

说实话，看到这篇文章下面的留言我很感动，为什么呢？因为我分享的测试思想得到了你的认可，也吸引你围绕着这一个主题展开了讨论。在这些留言中，我也仿佛看到了自己当年为了做好某一项测试而尝试的一系列方案。所以，在此我也想对你道声感谢。

在专栏的第 15 篇文章《过不了的坎：聊聊 GUI 自动化过程中的测试数据》中，我从创建测试数据的技术手段和时机两个方面，和你分享了在实际项目中，需要综合运用 API 调用和数据库操作来创建测试数据，并根据测试数据自身的特点，分而治之地采用 On-the-fly 和 Out-of-box 的方式，以寻求数据稳定性和数据准备效率之间的最佳平衡。

因为这个专栏后面有一个单独的系列去讲测试数据准备的那些事儿了，所以今天我就不再过多地展开这个问题了。当然，你在也可以在这篇文章下面，留下你关注的与测试数据相关的问题，等到测试数据准备系列的文章答疑时，我再挑选些典型问题进行解答。

在专栏的第 16 篇文章《脑洞大开：GUI 测试还能这么玩（Page Code Gen + Data Gen + Headless）？》中，我一下和你分享了页面对象自动生成、测试数据自动生成、无头浏览器这三种技术。或许，在你看来有些停留在概念层面了。

说到为什么我没有针对某一个具体的概念，展开分享，其实我是这么考虑的：页面对象自动生成，主要用到的就是 QTP 这个工具，而对这个工具的使用，你可以通过它的文档来轻松上手，并且这个工具的内部实现也没有对外公开，所以这部分内容完全从工具使用的角度来讲的话， 并不能发挥这个专栏的价值。同样地，测试数据自动生成、无头浏览器也是这个道理。我还是希望你能在知道了这些概念之后，可以结合自己的实际工作，动手实践，这样的效果才是最好的。

所以，今天我也就不再过多的展开这个话题了。如果你在落地这三个概念的时候有任何问题，也可以给我留言，希望可以帮到你。

在专栏的第 17 篇文章《精益求精：聊聊提高 GUI 测试稳定性的关键技术》中，我为你归纳了五种造成 GUI 自动化测试不稳定的主要因素，即：非预计的弹出对话框、页面控件属性的细微变化、被测系统的 A/B 测试、随机的页面延迟造成控件识别失败，以及测试数据问题。然后，我针对每种问题，给出了对应的解决思路。

当时为了不淹没提高 GUI 测试稳定性的关键技术这个主题，我并没有通过实例展开这五个因素及其对应的解决方案。今天为了加深你的理解，我决定针对随机失败的重试（retry）和你分享一个实例，作为今天我要回答的第三个问题。

在专栏的第 18 篇文章《眼前一亮：带你玩转 GUI 自动化的测试报告》中，我和你分享了一份理想的 GUI 自动化测试报告应该包括哪些内容，并和你分享了 eBay 在全球化 GUI 测试报告中的创新设计。希望这些方法，你也可以经过适当改进，用到自己的测试项目中。

有些读者感觉这篇文章不够过瘾，想要知道某一个技术细节的实现。所以，这里我会针对基于 GUI 测试报告，和你再分享一个小例子，和你说说故事板样式的 GUI 测试报告具体是如何实现的，也就是后面我要分享的第四个问题。

但是，我还是想要再强调一下，这篇文章的设计初衷并不是去解释每种报告的实现细节，而是帮助你从多元化的视角去思考一个高效的测试报告应该是什么样的。

在专栏的第 19 篇文章《真实的战场：如何在大型项目中设计 GUI 自动化测试策略》中，我从“实战”的角度，分享了实际的大型全球化电商网站的 GUI 自动化测试如何开展，希望可以帮你解决测试策略如何设计、测试用例脚本如何组织这两个问题。

其实，通过这篇文章，我已经和你说清楚了大型项目中的 GUI 自动化测试策略这个问题，所以在今天这篇文章中我就不再继续展开这个话题了。

在专栏的第 20 篇文章《与时俱进：浅谈移动应用测试方法与思路》中，我和你分享了 Web App、Native App 和 Hybrid App，这三类移动应用的测试方法，以及移动专项测试的思路与方法。其实，这三类移动 App 的测试，和 GUI 自动化测试的思想是相通的，只不过是针对移动应用自身的特点，又有了一些独特的方法而已。

在专栏的第 21 篇文章《移动测试神器：带你玩转 Appium》中，我用 Appium 手把手地带你实现了一个移动应用的测试用例，并本着知其所以然的原则，和你分享了 Appium 的实现原理，希望借此可以带你玩转 Appium，完成自己的移动应用测试。

这两篇关于移动 App 测试的文章更新后，大家留言都很踊跃，也可以反映出大家很关注这块内容。所以，今天我再选择微信小程序的自动化测试，和你再分享下业界的主流工具，以此作为今天的最后一个问题。

问题一：目前互联网企业在 GUI 测试上的投入在不断减少，而为什么我要花这么多精力去讲 GUI 测试？

这个问题，很多人都想知道答案。那么我就说说这么设计的考量吧。

当前互联网产品的测试重点的确逐渐在向后端（API）迁移，GUI 自动化测试所占比重越来越低，出现这种情况的原因主要有两个：

GUI 自动化测试用例的 ROI 一直上不去，投入较大。一方面，测试用例本身还很难做到 100% 的稳定；另一方面，被测系统界面发生变化时，测试用例的维护成本一直高居不下。

微服务架构的普及使得后端逻辑可以被重用到大量的前端界面中，比如同一个后端 API 既可以通过 PC 端的浏览器请求来访问，也可以通过移动端 Native App 的请求来访问，此时保证后端 API 的质量就成了保证前端质量的基础。也是因为这个原因，API 测试取代了部分 GUI 自动化测试。

这么说来，我们完全就应该把测试重点放在 API 端，而没必要花很大的精力去做 GUI 的自动化测试。但现实情况是，如果你没有站在终端用户的角度，对系统基本业务功能通过用户实际使用的 GUI 做过测试，你是否敢直接发布上线。

很多时候，后端 API 的功能都正常并不能保证前端的 GUI 功能测试就一定没有问题。所以，现在很多互联网企业依然会在 GUI 自动化测试上做投入，只不过是将 GUI 自动化测试用例的设计原则从原本“全面覆盖”，进化成为了“只覆盖最基本的核心业务功能”。

问题二：是否应该在页面对象模型中封装控件的操作？

这其实是个测试架构设计的问题，而且一直以来都有争议。

如果在页面对象中封装控件的操作，实现起来相对简单，而且符合传统的面向对象的设计思想，即：一个对象既有对象描述又有对象操作。

但是，从测试架构的层次上来看，这种设计不够规整，而且将对象的识别和操作进行了耦合。而这种耦合就意味着需要对对象的识别和操作一同进行更新和版本管理，但现实情况是对象的识别需要经常更新，而对象的操作相对来说很稳定，这种不必要的依赖关系又增加了测试的维护成本。

如果将页面对象和对象的操作分开封装，即：让对象操作的类依赖于对象定义的类。这种设计，在测试架构的层次上来看比较清晰，而且可以分别来实现页面对象识别和对象操作的版本管理。同时，如果你已经采用了页面对象自动生成技术，就可以直接生成页面对象类，从而保证页面操作类的稳定。这样做的好处也很明显，即能够以自动化的方式来降低由页面变化引起的改动工作量。

由此可见，这两种方式各有优劣。那么，根据我的经验来看，对于初级阶段的 GUI 自动化测试，直接在页面对象中封装控件操作的方式更简单直接；而随着 GUI 自动化测试不断成熟，在需要考虑引入页面对象的版本管理以及页面对象的自动生成的时候，将页面对象和对象的操作分开封装的方式，就是大势所趋了。

问题三：如何通过重试（retry）机制，来应对由随机的页面延迟造成的控件识别失败？

正如我在第 17 篇文章《精益求精：聊聊提高 GUI 测试稳定性的关键技术》中提到的，重试可以是步骤界别的，也可以是页面级别的，甚至可以是业务流程级别的。

而在这其中，最下面的一层重试机制是在控件操作这一层的。比如，控件识别失败的时候，我们会在自动化测试框架层面发起重试。一般来讲，我们会在自动化测试框架中的控件操作函数上封装重试逻辑，而且默认情况下，我建议你可以启用这个级别的重试。

再往上一层是业务流程，也就是 Business Flow 的重试。如果一个业务操作失败了，我们就可以考虑重新执行这个业务流程。

但是，这其中需要特别注意的是， 业务流程对数据本身可能存在依赖，所以业务流程重试的合理性需要根据测试用例的上下文来决定，尤其要关注测试数据是否可以支持可重复执行。

一般来讲，我们会在自动化测试框架中实现 Business Flow 的重试。但是，我建议默认关闭该功能，只在你确定需要进行业务流程级别的重试时才显式启用 Business Flow 层面的重试。这么做的主要原因是，业务流程的重试会涉及测试数据的问题，只有当你非常肯定业务流程的重试不受测试数据的影响时，我们才会人为启用业务流程级别的重试。

最上面一层是测试用例级别的重试。当一个测试用例执行失败的时候，我们可以考虑重新执行整个测试用例，一来希望排除随机出错的可能，二来是评估出错的步骤是否可以重现。

这种测试用例级别的重试，一般是由 CI 流水线完成的，而不是由自动化测试框架实现的。CI 流水线会整理出所有失败的测试用例列表，然后在 CI 流水线脚本中发起重试。

问题四：故事板样式的 GUI 测试报告，具体是如何实现的？

故事板样式的 GUI 测试报告，指的是按时序对 GUI 操作界面进行截图生成的测试报告。对于这类报告的实现主要考虑两个方面的因素：

一是，获取屏幕截图。这一步，我们可以通过封装控件操作函数来实现，比如在自行封装的 click 函数中，先调用截图函数然后再调用真正的 click 操作。

二是，截图在报告中的展现形式。这一步，我们可以直接使用现成的 HTML5 的 PPT 框架，这样我们只要按照框架要求来生成屏幕截图的数据结构即可，而无需去关注复杂的 HTML5 的处理逻辑。

这里，我强烈推荐 reveal.js。eBay 就是用这个框架开发了自己的故事板样式的 GUI 测试报告。其中，在第 18 篇文章《眼前一亮：带你玩转 GUI 自动化的测试报告》中的测试报告截图就是基于 reveal.js 实现的。关于 reveal.js 的详细用法和数据结构，请参考这里。

问题五：关于微信小程序的自动化测试，以及用到的测试工具。

很多读者在留言中问到了微信小程序的测试问题，这里我稍微展开下。

其实，微信小程序的测试，在测试设计和执行思路上同移动端测试并没任何区别。那为什么很多人会问到微信小程序的测试呢？

我觉得主要原因是，在实际开展自动化测试的时候，由于缺少专业的、有效的工具，使得微信小程序的测试在我们看来难以执行。

其实呢，有一款来自腾讯的微信小程序测试工具 XTest 就是个不错的选择，可以很方便地支持测试用例的录制。这里有一篇使用这个工具进行实际小程序测试的文章，你可以从中获得一些使用细节。

最后，感谢你能认真阅读第 12~21 这 10 篇文章的内容，并写下了你的想法和问题。期待你能继续关注我的专栏，继续留下你对文章内容的思考，我也在一直关注着你的留言、你的学习情况。

感谢你的支持，我们下一期答疑文章再见！

测试专栏特别放送 | 答疑解惑第二期

你好，我是茹炳晟。

今天这篇文章是“答疑解惑”系列文章的第二期，我还是选取了五个问题。这五个问题来自于第 6~11 篇这 6 篇文章，其中第 9 和第 10 篇这两篇文章中的两个问题被我合并为了一个问题，并且我会针对这个问题，再次为你简单梳理一条学习路径。

然后，我还会选择这 6 篇文章下的精彩留言，为你稍作解答、分析。

在这篇文章中，我依旧为你添加了每篇文章的链接，并用一句话概括了这篇文章的主要内容，你也可以再次回到这篇文章中，回忆一下第一次阅读后的想法，看看第二次阅读又有了哪些新的感想。欢迎你继续给我留言，我仍在关注着你的问题、反馈，希望可以为你提供更多的帮助。

现在，我们就开始今天的五个问题吧。

问题一：你在使用代码覆盖率工具的时候，遇到过哪些“坑”？

在专栏第 6 篇文章《你真的懂测试覆盖率吗？》中，我针对代码覆盖率这个主题，和你分享了代码覆盖率的的价值、局限性，并结合 JaCoCo 分析了代码覆盖率工具的实现原理。这也是这个专栏中第一篇为你讲解某种测试工具实现原理的文章，希望你可以认真体会。

通过阅读这篇文章，我希望你回顾一下曾用过的代码覆盖率工具，并说说你的使用感受和遇到的“坑”。

留言中，我也看到了一些留言很精彩，提到了自己使用代码覆盖率工具的一些实践。所以，也很感谢你们的分享。

接下来，我再和你分享下，在使用代码覆盖率工具时可能面临的两个最大的问题：

测试覆盖率越往后越难。

统计代码覆盖率的根本目的是，指导用户的测试用例设计。也就是说，我们要通过代码覆盖率的结果去发现哪些代码没有被执行到，以此为依据再去设计有针对性的测试用例。

在这个过程中，你会发现前期达到一个不错的覆盖率指标（比如 70%）还是比较容易的，但是越往后就越难提高了。

因为，后期没有覆盖到的代码往往都是一些出错异常分支的处理，为了能够覆盖这部分内容，往往需要构造特殊的数据和环境。很多时候，这些数据和环境并不好得到，就不得不采用各种 Mock 的手段来实现。

因此，在实际项目中，到底要不要一定到达很高的覆盖率，就应该根据项目情况结合风险驱动的概念来综合分析了。

推行代码覆盖率的初期阶段，很难要求很高的覆盖率指标。

代码覆盖率的挑战并不是来自于技术本身，而是来自于管理。很多公司在刚刚推行覆盖率统计的时候，会发现各个模块和项目的覆盖率普遍较低，有些甚至还不到 20%。这时，我们就不应该依靠行政手段来强行规定高的代码覆盖率。

因为这会在短时间内增加很多工作，一来会引起开发人员的抵触与反感，二来会耽误项目本身的进度。此时最好的做法是采用持续改进的策略，也就是随着迭代更新，代码覆盖率不允许出现下降的趋势，至少保持持平或者逐渐增长的态势。

问题二：你在填写软件缺陷报告时，还有哪些好的实践值得分享呢？

在专栏第 7 篇文章《如何高效填写软件缺陷报告？》中，我分享了想要把发现的缺陷准确无歧义地表达清楚，一份高效的软件缺陷报告应该包括的内容，以及需要注意的问题。

在这篇文章最后，我希望你分享一下自己在填写软件缺陷报告时，还有哪些好的实践。

在这里，我想再和你分享另外一个观点。你在实际提交软件缺陷的时候，有没有感觉这个过程很繁琐。对于缺陷的重现规律和步骤需要做很多尝试，然后写文档时还有很多工作量，比如需要考虑措辞和语句的组织，这往往会花费你不少时间。那有没有什么好方法可以减少写文档的工作量吗？

其实，对于传统软件的开发流程来讲，这种重量级的缺陷报告是必须的。特别是对于一些大公司来说，开发人员和测试人员可能散布在全球不同时区的地域，这种严格的缺陷文档就很有必要了。

但是，现在的很多互联网企业，尤其是国内的互联网企业，所有的工程技术人员都在一个办公室，而且普遍采用敏捷开发的模式，此时面对面地沟通软件缺陷的效果将远远好于文档描述，而缺陷报告本身的作用也会退化成一条简单的记录。

所以，缺陷报告的详细程度应该在很大程度上取决于团队特征。

另外，我发现读者在文章的留言中也提出了很多建设性的方法。比如，下面这位昵称为“卫宣安”的读者，提出了让开发人员主动来认领缺陷的方式。如果说所有的开发人员都具有很强的责任心，同时系统规模也不是太大，或者说报告的缺陷数量不是很多的情况下，这的确是解决问题的一个好思路。但是，当团队规模比较大，缺陷数量也比较多的时候，要求每个开发人员都去挨个查看所有缺陷的效率就会很低。

那么，在这种情况下，我们是否有更好的方法来解决这个问题呢？答案就是，采用基于 AI 和机器学习的缺陷分类方法。我们可以通过缺陷的特征值提取，并结合分类算法对缺陷应该归属的团队进行自动分类。

在 eBay 的自动化测试体系中，就有专门的系统对失败用例和缺陷做自动进行分类。

问题三：你觉得在实际工程项目中，一份高效的测试计划应该包括哪些内容？

在第 8 篇文章《以终为始，如何才能做好测试计划？》中，我和你分享了虽然在敏捷开发模式下，软件测试不再局限于厚重的、正式的计划文档，但是测试计划的重要性丝毫没有发生变化。一份成功的测试计划，依旧必须要清楚地描述出测试范围、测试策略、测试资源、测试进度和测试风险预估这五个最重要的方面。

而在读完这篇文章之后，我希望你思考的是，在一份测试计划中，除了这五个最最关键的内容外，你觉得在实际工程项目中还需要再增加的内容，以及是不是所有的项目都需要有很详实的测试计划。

其实很多时候， 你会发现计划的速度远远赶不上变化，尤其是互联网产品的开发。就像下面这两位用户在留言中描述的现象，相信你在实际的工作中一定遇到过类似的场景。

所以，这个时候，你有没有反问过自己一个问题，此时文档化的详细测试计划还真的有必要吗？或者说有没有可能采用轻量级的测试计划。

首先，轻量级的测试计划并不是说没有计划，而是指计划的文档化表现形式应该尽可能简单，只是用一些关键词来描述纲领性的东西。这样做，一来可以降低测试计划本身的写作时间；二来当测试计划由于各种原因发生变化的时候，也可以非常快速灵活地进行修改和更新。

其实，目前的敏捷开发模式（比如 Scrum 模式）下的测试计划就会在每个 Sprint 最开始的时候，以非常轻量级的方式来确定测试计划，有些时候甚至可以没有文档化的测试计划。这时，关于测试范围、测试策略和测试设计之类的内容都在测试人员的脑子中。

注意，这时候虽然没有书面的测试计划，但并不代表说没有测试计划。

问题四：软件测试工程师的高效进阶路径是什么？

我在第 9 篇文章《软件测试工程师的核心竞争力是什么？》和第 10 篇文章《软件测试工程师需要掌握的非测试知识有哪些？》中，分别和你分享了一个软件测试工程师需要具备的核心竞争力，以及需要掌握的非测试专业知识。看到这两篇文章后面，有很多用户留言说：觉得很迷茫，抓不住自己要重点培养的能力、要怎么快速学习。

虽然今年 7 月 6 日我在极客时间做直播时回答过这个问题，但这里为了帮助你快速找到一条适合自己的道路，我就再简单为你梳理下。

首先，我把在软件测试岗位上工作了 0~5（或者 0~3）年的工程师，划分为初级测试人。为什么有这个划分呢？因为 0-5 年工作经验的测试工程师往往工作的中心都还是在软件产品测试本身，还没有将测试上升到质量工程的高度。当然了，我这里说的测试指的是广义上的测试，包括功能测试、自动化测试、性能测试等各个方面。

然后，我把初级测试人可以进阶的方向，归纳为了三类：

业务专家，也就是业务功能测试方向；

开发测试工程师，也就是自动化测试方向，指的是把业务功能的测试转换成自动化的脚本；

测试开发工程师，指的是负责开发测试平台、工具，以及服务。

接下来，我就简单总结下，向每个方向进阶的相对高效的路径吧。

首先，如果你想成长为一名业务专家的话，那你就需要精通于某一项具体的测试业务，比如说电子商务网站、EPR 系统和 SAP 系统等等。这样的角色更像是产品经理，你需要能准确把握业务产品的定位，了解整个业务流程的操作、用户的使用习惯，以及如何提到整个产品的转化率。

而要成为这样的人，你首先需要在该领域中有较长时间的积累，能够从真正的终端用户视角来使用被测软件，能够对该软件应用领域的行业知识有比较清楚的了解。

但精通于某一个业务领域的缺点是，一旦离开了这个业务领域，之前的业务积累就没用了。

其次，如果想要成长为一名开发测试工程师的话，你平时需要积累高效的测试用例组织方法、对自己用到的测试框架的优劣势有深入理解，并在使用某种测试工具时要深入到其原理的层面。

这样的话，你就可以快速具备测试用例设计、测试框架选型、灵活运用测试工具的技能。并且，你也会因为这种长期的“知其所以然”的积累，可以从容应对新的技术趋势。比如说，你掌握了 Selenium 1.0、2.0 的实现原理后，对 API 测试的原理也就可以做到触类旁通了。

最后，如果想要成长为一名测试开发工程师的话，那你需要培养的是开发能力，以及测试意识。说白了，测试开发工程师，更像是一个开发人员，只不过需要在理解测试上下文的基础上，为其他测试工程师开发一些平台、工具、服务。这时，我建议的成长路径，就是一个开发工程师的成长路径了。

问题五：你觉得你现在团队采取的自动化测试策略，有哪些好的地方，又有哪些需要改进的？

在第 11 篇文章《互联网产品的测试策略应该如何设计？》中，我和你分享了互联网产品的特性决定了它采用的测试策略，遵循的是“重量级 API 测试，轻量级 GUI 测试，轻量级单元测试”的原则，更像是一个菱形结构。这与传统软件产品的金字塔测试策略有所区别。

而在文章最后，我希望你可以针对你所在公司采取的测试策略，谈谈自己的看法。

这里，我可以给你分享一下 eBay 的自动化测试策略的演变和发展路径，希望可以帮助你理解什么叫作因地制宜地选择和设计最适合你的自动化测试策略。

eBay 在早期阶段，也和现今大多数公司一样，采用的是基于 GUI 来大规模开展自动化测试。尤其早期阶段的网站本身还不是太复杂，而且还不是现在的微服务架构，所以基于 GUI 的自动化测试在页面对象模型和业务流程模型的支持下，能够很好地完成业务功能的验证和测试。

这个阶段，我们关注的重点是通过 GUI 层面的操作来对整个网站的业务功能进行验证。

可是，后来随着业务的不断发展与壮大，基于 GUI 的测试用例数量不断增长，同时再加上浏览器兼容性测试等的要求，测试用例的数量越来越多，测试执行的效率也越来越低下，所以单靠 GUI 测试已经很难满足全面测试的要求了。

再加上系统架构本身也从原本的单体应用逐渐发展成为了微服务，甚至是服务网格的架构形式，同时普遍采用了前后端分离的架构设计，所以此时的测试重点也就从原来以 GUI 为主转变成了以后端 API 为主的阶段。

此时 GUI 测试只会去覆盖一些最最基本的业务功能，而 API 测试则会关注各种参数的组合以及各种边界场景。

通过这个实际的例子，我们可以看出，并不存在一个放之四海而皆准的自动化测试策略。测试策略的选择，在很大程度上取决你的测试诉求以及被测系统本身的架构设计。

最后，感谢你能认真阅读第 6 到 11 这六篇文章的内容，并写下了你的想法和问题。期待你能继续关注我的专栏，继续留下你对文章内容的思考，我也在一直关注着你的留言、你的学习情况。

感谢你的支持，我们下一期答疑文章再见！

测试专栏特别放送 | 答疑解惑第五期

你好，我是茹炳晟。

今天的“答疑解惑”系列文章，我们一起来解决性能测试系列中 7 篇文章的问题。你可以通过下面对每篇文章的简单总结回顾一下文章内容，也可以点击链接回到对应的文章复习。

现在，我们就开始今天的问题吧。

问题一：你在性能测试项目中，选择具体的系统吞吐量指标时，会考虑哪些因素呢？

在专栏的第 28 篇文章《带你一起解读不同视角的软件性能与性能指标》中，我首先从终端用户、系统运维人员、软件设计开发人员和性能测试人员，这四个维度介绍了软件系统的性能到底指的是什么；然后，和你分享了软件性能的三个最常用的指标：并发用户数、响应时间、系统吞吐量。

而在这篇文章最后，我针对系统吞吐量提出了这样一个问题：系统吞吐量的表现形式有很多，比如“Requests/Second”“Pages/Second”“Bytes/Second”等，你在性能测试项目中，选择具体的系统吞吐量指标时，会考虑哪些因素呢？

其实选择哪种类型的吞吐量指标，和你的测试目标以及被测系统的特点是息息相关的。

如果你的被测系统是后台处理系统，而且你的测试目标是要优化它的处理能力，那么这个时候你的关注点必然就是每秒能够处理的请求数量，即 Requests/Second。当然如果你发现你的后台处理能力有可能是受限于网络传输的带宽，那么这个时候你就可能需要去考虑“Bytes/Second”这种类型的吞吐量指标了。

总结来讲，选取哪个吞吐量指标，取决于你最关注的内容。

下面这位昵称为“假装乐”的读者的留言很典型，是很多刚刚结束性能测试的同学都会有的疑惑。

其实，性能测试应该贯穿于软件研发生命周期的各个阶段：

单元测试阶段就要衡量代码级别的时间复杂度和空间复杂度，以及多线程并发情况下的功能准确性等等；

每个 API 也需要进行单独的性能测试和评估；

集成测试阶段需要考虑跨组件或者模块的数据大小，以及缓存的使用情况等等；

系统测试阶段，还需要从模拟终端用户负载的角度来衡量系统全局的性能指标等等。

说到底，一个最基本的原则就是，性能问题一定是越早发现越容易定位，也越容易被修复。而到了软件研发生命周期的后期，性能问题的定位成本和复杂度会呈指数级增长。

所以，如果你有机会去了解大型软件公司的测试的话，就会发现它们没有所谓的性能测试团队，而是有一个性能工程团队。这个团队会从软件研发生命周期的各个阶段去测试、评估和优化软件的性能。

问题二：你在实际工程项目中，接触过性能测试的哪些方法，其中遇到了哪些问题，又是如何解决的？

在专栏第 29 篇文章《聊聊性能测试的基本方法与应用领域》中，我通过一个医院体检的例子，和你分享了并发用户数、响应时间和系统吞吐量这三个指标之间的关系和约束；然后，又和你分享了性能测试七种常用方法，以及四大应用领域。

在这篇文章最后，我希望你能够分享一下你在实际开展性能测试时，都遇到过哪些问题，又是如何解决的。虽然这篇文章的留言比较少，但也能从中看出大家在开展性能测试的时候，确实也如我当初一样，遇到了各种各样的问题。

那么，现在我就来和你分享一下性能测试中可能遇到的一些典型问题吧。

其实，性能测试中可能会遇到的问题实在是太多了，架构中的各个层面、每个软件模块、模块配置、数据库中的数据量、多线程的锁机制、进程设计、JVM 配置参数、数据库配置参数，以及网络参数等等，都会成为性能测试中的问题。

可以说，性能测试的问题，只有你想不到的，没有你遇不到的。所以，如果我通过一个实际案例和你分享的话，肯定会是长篇大论，有违答疑系列文章的设计初衷。为什么？因为性能测试的问题，一般都和架构、设计、配置、数据量有着密不可分的关系。所以，我今天会通过一个简化的案例，和你展开分享，意在抛砖引玉。

首先，我想问你一个问题：当你做压力测试的时候，你觉得硬件资源占用率是低好，还是高好？很多人可能会说，当面对大量并发请求的时候系统资源占用率当然低好。因为资源用得少，说明系统后续的容量可以继续大幅度扩充。

听起来很有道理，但真的是这样吗？

我就遇到过一个测试，当你不断加大并发访问量的时候，系统 CPU 的使用率一直处在 15% 左右，不管并发用户数如何加大，CPU 的使用率一直上不去，但是事务响应时间却随着并发用户的上升而有持续上升的趋势。所以，一定是有某些机制限制了 CPU 的使用。

其实，在这种情况下我们希望看到的是，随着并发用户数的不断增长，这些 CPU 敏感性的并发操作会尽可能多地去使用 CPU 的计算能力，而不是现在这种 CPU 使用率上不去的情况。

为此，我分析了这部分的代码逻辑，发现其中使用了一个固定大小的数组来存放并发任务进程的句柄，当这个数组满了的时候，新进程的创建处于阻塞状态，只有之前的进程处理结束后，数组中出现了空位，新的进程才会被创建。当时这个数据的大小是 128，也就是最多只能有 128 个并发进程同时运行，我当时就怀疑这是限制 CPU 使用率的主要原因。

为了验证这个想法，我直接将这个固定数组的大小调整成了 256，然后继续并发测试。果然，CPU 的使用率徘徊在 30% 左右，就验证了我的猜测。

那么，接下来就需要修复这个问题了。显然，这是一个设计上的问题，压根儿这里就不应该采用固定大小的数组，而是应该采用可变长度的数据结构。

问题三：你接触过哪些后端性能测试工具？你认为这款工具中，有哪些好的设计吗？

在专栏第 30 篇文章《工欲善其事必先利其器：后端性能测试工具原理与行业常用工具简介》中，我以问答的形式，和你分享了后端性能测试的理论，以及工具使用的问题。这也是这个专栏中，唯一一篇采用问答形式的文章，有没有感觉读起来比较省力呢？

因为我后面增加了一篇 JMeter 的加餐文章，所以这里我也就不再过多地介绍后端性能测试工具了。这次，我来回答一下“Robert 小七”提到的问题。

正如我在今天的第一个问题中提到的，高效的性能测试一定是从源头抓起的，也就是研发的每个阶段都需要进行性能测试，而不是等到系统开发完了，再一次性地进行黑盒级别的性能测试。

所以，对每个 API 的性能测试是非常必要的。而且，很多公司，比如 eBay 等，都会对每个 API 进行独立的性能测试。其实，在对 API 开展独立的性能测试之前，还需要在一些关键代码上做基于函数方法的性能测试和评估。直到这些都完成了以后，才会开展基于性能场景的测试。

问题四：你在工作中接触过哪些前端性能测试工具，它们各自有什么特点呢？

在专栏的第 31 篇文章《工欲善其事必先利其器：前端性能测试工具原理与行业常用工具简介》中，我以一个具体网站为例，和你分享了使用 WebPagetest 进行前端性能测试的方法，以及前端性能相关的主要概念与指标。

在这篇文章最后，我希望你可以分享一下自己曾使用过的前端性能测试工具。

在这里，我来分享下我的经验吧。

前端性能测试工具除了我在文章中介绍的 WebPagetest，比较常用的还有 YSlow，但是这些工具的基本原理是类似的，所以如果你已经掌握了我在这篇文章中介绍的 WebPagetest 的原理的话，对于 YSlow 等前端性能测试工具的原理，基本就可以做到触类旁通了。

此外，有些公司还会特别关注一些特定的前端性能指标。这些性能指标，一般不能从性能测试工具中直接得到，需要自行定制开发。

这个昵称为“木然”的用户，提出的问题很典型。很多刚开始使用 WebPagetest 的同学都会有这个疑问，但是很不幸，WebPagetest 是无法来做这种需要登录才能访问到的页面的前端性能调优的。

WebPagetest 这类工具的初衷，就是纯粹站在前端页面优化的角度来设计的，本身并不会涉及业务操作，所以对这块的支持很弱。虽然 WebPagetest 支持 Http Auth 以及自定义脚本的扩展，但是 Http Auth 还是会受到服务器端本身配置的影响，而自定义脚本的扩展还受限于特定的浏览器，所以实际的应用价值有限，也很少有人去用。

而至于有什么更好、更灵活的方法来处理这种需要登录，以及特定业务操作的前端页面性能优化，很可惜，目前我并没有什么好的方案。如果你对此有一些好的想法或者实践的话，就给我留言一起讨论吧。

问题五：在实际工作中，获取并细化性能测试需求，需要怎么做，注意哪些问题呢？

在专栏的第 32 篇文章《无实例无真相：基于 LoadRunner 实现企业级服务器端性能测试的实践（上）》和第 33 篇文章《无实例无真相：基于 LoadRunner 实现企业级服务器端性能测试的实践（下）》中，我从最基础的性能测试需求获取、LoadRunner 的原理开始，和你分享了基于 LoadRunner 实际开展企业级服务器端性能测试的整个过程。

通过这两篇文章，我希望能够帮你快速建立服务器端性能测试的全局观，并了解各主要步骤的关键技术细节。其实，正如我在文中所说的，在开展整个性能测试的过程中，测试需求的获取是其中最关键、最难的一个环节。这里我再针对测试需求的获取，和你分享一个实例吧。希望可以真正帮到你。

很多时候，我们从产品经理那里拿到的需求是很笼统的，必须经过必要的分析和细化才能转换为可以用于性能测试场景设计的需求，就像文章中提到的“每天支持完成 8000 个体检”的例子。这样的例子还有很多，比如我们经常可以看到类似“系统最大支持 500 万用户同时在线”的需求，这同样是一个看似具体，实则非常笼统的性能需求。

500 万用户在线，这些在线的用户在具体执行什么类型业务操作，对后端服务器造成的压力差别是巨大的。比如，这 500 万个用户都在执行查询操作和这 500 万个用户什么不做，对后端服务器的压力是天壤之别的。

那么，这里需求获取的难点就是，要能够准确估算这 500 万在线用户执行的各种类型的业务操作所占的百分比，只有这样才能真实、客观地反应后端服务器承受的实际压力。

但是除此之外，还有很多的性能需求并不是直接从产品经理那里获取的，而是需要资深的性能测试人员根据以往的经验，以及同类系统和竞品的业务流量来自己估算。

比如，产品经理不会告诉你一个实现具体业务的 API 操作应该要在多长时间内完成；产品经理也不会明确告诉你在 API 层面的业务吞吐量是多少。这些测试需求都是需要性能测试人员来预估，甚至是基于一些实验来细化的。

所以说，性能需求的获取是一个关键且困难的环节。

这个昵称为“Sunshine”的用户，在留言中的问题虽然简单，但也是个典型问题。我来和你一起分析一下。

关于如何实现每隔 10 s 增加 100 个用户的方法，其实还算是简单。LoadRunner 的场景设计界面中，直接提供了该功能。你可以通过 GUI 界面填写你的用户增加策略，比如在什么时间段内每多少秒增加或者减少多少个并发用户，并且 LoadRunner 还会自动提供并发用户数随着时间变化的曲线图。你甚至可以直接修改这个曲线图，来修改用户数量变化的规律，使其符合你的需求。

另外，场景设计中的很多配置都可以在 LoadRunner 的场景设计界面中实现。具体内容，你可以参考LoadRunner 的使用文档。

问题六：你所在企业，在开展性能测试时采用了哪些方法呢？

在专栏的第 34 篇文章《站在巨人的肩膀：企业级实际性能测试案例与经验分享》中，我挑选了最重要的四类性能测试方法（性能基准测试、稳定性测试、并发测试，以及容量规划测试），和你分享如何在实际项目中完成这些测试，确保软件的性能。

通过这篇文章，我希望可以帮助你从整体上理解性能测试，形成一个体系知识。而在这篇文章最后，我希望你能够分享一个你所在企业采用的性能测试方法，大家互相取长补短。

对于 eBay 这样的大型全球化电商企业，性能测试除了文章中提到四类性能测试方法以外，还会开展一些其他类型的性能测试。

对于关键业务代码以及中间件代码的核心算法部分，一般都会开展基于时间复杂度和空间复杂度的代码级别的性能评估；

对于各个独立的中间件或者公共服务组件本身，也会开展性能基准测试和容量规划测试；

对于基于微服务的各个 API 接口，会开展性能基准测试和压力测试；

对于前端 Web 页面，会开展基于前端性能的调优；

对于整体系统，会不定期开展全链路压力测试，其中还会使用历史流量回放等技术来模拟海量实际的并发场景。

以上这些是从性能测试的类型来讲的。从性能测试工具的支持上来看，eBay 还建立了一些内部使用的公共性能测试平台，任何人都可以通过这些性能测试平台方便地发起压力负载，而不用去关心诸如 Load Generator 之类的细节，对于后端性能测试以及 API 性能测试，你只要上传压测脚本和性能测试场景设计，就能很方便地发起测试。这个很像淘宝对外提供的 PTS 服务。

其实，上述的这些方法适用于很多的互联网产品。而至于到底实施哪几条，则取决于你想通过性能测试希望达到什么样的目的。

最后，感谢你能认真阅读第 28~34 这 7 篇文章的内容，并写下了你的想法和问题。期待你能继续关注我的专栏，继续留下你对文章内容的思考，我也在一直关注着你的留言、你的学习情况。

感谢你的支持，我们下一期答疑文章再见！

测试专栏特别放送 | 答疑解惑第六期

你好，我是茹炳晟。

今天的“答疑解惑”系列文章，我们一起来解决测试数据准备和测试基础架构这两个系列 8 篇文章中的问题。你可以通过下面对每篇文章的简单总结回顾一下文章内容，也可以点击链接回到对应的文章复习。

这两个系列下的文章留言已经很少了，或许是你没有坚持学习，也或许是这部分内容并没有切中你现在的痛点。毕竟，广义上的软件测试，包括了测试数据平台、测试执行平台等，而我们也不能每一种都有机会接触。

但这里，我想再给你打打气，有些知识虽然你在接触时感觉自己不会用到，但随着技术发展、公司业务转型，或者是你个人的职业晋升，都会需要越来越宽广的知识面，这也正应了我在专栏里面提到的一个比喻，测试工程师通常是“广度遍历”，关注的是“面”。所以，坚持学习，才是我们从“小工”蜕变为“专家”的正确路径。

问题一：有些时候，我们需要创建消息队列里的数据，这类数据应该如何创建呢？

在第 35 篇文章《如何准备测试数据？》中，我从测试数据创建的维度，和你详细分享了生成测试数据的四种方法：基于 GUI 操作生成测试数据、通过 API 调用生成测试数据、通过数据库操作生成测试数据，以及综合运用 API 和数据库的方式生成测试数据。

其实，我们要创建的测试数据并不仅仅局限于数据库，很多时候还需要创建消息队列里面的数据。所以，在阅读完这篇文章后，我希望你可以思考一下如何处理这类问题？或者，请你分享一下你曾经是如何解决这个问题的。

这里，我来分享下我的方法吧。

通过模拟消息队列中的测试数据，可以实现各个被测模块之间的解耦，这个思路非常关键。至于如何来模拟消息队列中的测试数据，在技术上其实没有任何难度。

我们通常的做法是，在测试数据工具的底层封装一个工具类，这个工具类内部通过调用消息队列的 API 或者操作接口函数来实现消息队列的 CRUD 操作，然后凡是需要改变消息队列中数据的地方，都通过这个工具类来完成实际操作。

问题二：你所在公司，采用是什么测试数据策略？为什么选用了这种策略？

在专栏的第 36 篇文章《浅谈测试数据的痛点》中，我和你分享了选择不同时机去创建测试数据，是为了解决不同的数据准备痛点。为了解决这些痛点，我的经验是把测试数据分为“死水数据”和“活水数据”，其中：“死水数据”适合用 Out-of-box 的方式，而“活水数据”适合采用 On-the-fly 的方式。

在这篇文章最后，我希望你可以分享一下自己项目中采用的是什么测试数据准备策略，以及会不会使用线上真实的数据进行测试。

这里我来分享下 eBay 在准备测试数据时的策略吧。

eBay 在准备测试数据时，采用的并不是单一的某种方法，而是针对实际的业务场景选取了不同方法，最后将这些方法进行组合完成整个测试的数据准备工作。可谓是多管齐下。

这里，我和你分享下 eBay 主要使用了的几种策略：

能用 API 的地方就一定不用数据库操作；

数据库操作只用在 API 无法支持，以及需要批量创建性能测试数据的场景；

对于“活水数据”，比如订单和优惠券等，一定采用 On-the-fly 的方式实现测试数据用例内的自维护；

对于“死水数据”，比如商品类目和品牌等，一定采用 Out-of-box 的方式来提高测试数据准备的效率；

对于性能测试的背景数据，采用生产环境的实际数据（注意，这里使用的实际生产数据是经过了必要的“脱敏”操作的）；

对于复杂数据，采用了预先在系统中预埋 template 数据，然后在需要使用的时候，通过复制 template 数据然后再修改的策略；

对于生产环境的测试，除了全链路压力测试，都会采用真实的数据来进行。

所以，测试数据策略的选择，最重要的是适合自己所在的公司或者项目的实际情况。这里我和你分享的 eBay 的实践，希望可以在针对特定场景选择测试策略的时候，可以感到有据可依。

问题三：如果你所在公司，也处于测试数据 1.0 时代，你们还用到过哪些测试数据准备函数的实现方法吗？

在专栏的第 37 篇文章《测试数据的“银弹”- 统一测试数据平台（上）》和第 38 篇文章《测试数据的“银弹”- 统一测试数据平台（下）》中，我从全球大型电商企业早期的测试数据准备实践谈起，和你一起分析这些测试数据准备方法在落地时遇到的问题，以及如何在实践中解决这些问题。

我希望通过这种遇到问题解决问题的思路，可以带着你去体会时代的演进，理解测试数据准备技术与架构的发展历程，并进一步掌握 3.0 时代出现的业内处于领先地位的“统一测试数据平台”的设计思路。

正如我在文中所说，目前大多数企业都还处于测试数据 1.0 时代。其实，这也很正常。因为，1.0 时代的测试数据准备函数，才是真正实现数据创建业务逻辑的部分，后续测试数据准备时代的发展都是在这个基础上，在方便用户使用的角度上进行的优化设计。所以说，测试数据 1.0 时代是实现后续发展的基础。

其实，对于测试数据准备函数的内部逻辑实现来说，除了我在文章中提到的基于 API、数据库，以及 API 和数据库相结合的三种方式以外，还有一种方法也很常用，尤其适用于没有 API 支持的复杂测试数据场景。

这种方法就是，事先在数据库中插入一条所谓的模板数据，在下一次需要创建这类数据的时候，就直接从模块数据种复制一份出来，然后再根据具体的要求来修改相应的字段。

这里需要特别注意的是，对于 1.0 时代的测试数据准备函数来说，我们还需要建立工具的版本管理机制，使其能够应对多个不同的数据版本。

问题四：目前 Selenium Grid 已经有 Docker 的版本了，你有没有考虑过可以在云端搭建 Selenium Grid 呢？

在专栏的第 39 篇文章《从小作坊到工厂：什么是 Selenium Grid？如何搭建 Selenium Grid？》中，我从测试基础架构的概念讲起，并和你分享了传统 Selenium Grid 和基于 Docker 的 Selenium Grid 的搭建方法。

不知道，你有没有在课后去尝试搭建 Selenium Grid 呢，这其中是否遇到了什么问题？如果你遇到了问题，欢迎你给我留言，我将帮助你一起解决。

在这篇文章最后，我希望你可以畅想一下是否可以在云端搭建 Selenium Grid。这里我结合 eBay 的探索，来谈谈我的看法吧。

对于一些大公司来说，在云端来搭建 Selenium Grid 已经被证明是切实可行的，而且也已经呈现出逐渐向云端过度的趋势。这主要得益于云端部署的易维护性和上云本身的便利性。

比如，eBay 已经实现了在 PCF 上基于 Docker 来运行 Selenium Grid 的方案，其中落地的难点在于配置 Docker 的网络和 IP 地址。主要原因是，PCF 会为部署的应用提供统一 App URL 的命名转换。

从本质上讲，只要 Selenium Grid 是基于 Docker 实现的，那么上不上云本身并没有本质区别。但是，考虑到将来的 App 以及所有的后台服务都会逐渐向云端过度，所以测试基础架构这块必然也会遵循这个趋势，和 App 以及后台服务的环境保持在一个技术栈上，将会减少公司整体基础架构的多样性，从而提高研发效能。

问题五：你觉得测试基础架构的设计和搭建过程中，还有哪些点需要再优化呢？又可以从哪些方面进行创新呢？

在专栏的第 40 篇文章《从小工到专家：聊聊测试执行环境的架构设计（上）》和第 41 篇文章《从小工到专家：聊聊测试执行环境的架构设计（下）》中，我首先和你分享了测试执行环境的概念，然后为你剖析了测试基础架构的演进历程，希望可以帮助你理解测试基础架构的设计，最终可以定制一套适合自己的测试基础架构。

学习完这两篇文章，我希望你思考的是，你所在团队，还可以再测试执行环境的架构设计上，进行哪些优化和创新？

其实，测试基础架构的优化和创新都是由问题本身驱动的。如果不是为了解决实际遇到的问题，企业是不会为了追求更新的技术而去寻求测试架构的改进方案的。所以，通常情况下，我们都是在测试或者 DevOps 的过程中，遇到了问题或者瓶颈才会考虑新的技术与方法。

比如我在文中提到的，为了解决大量测试用例在短时间内执行完成的要求，才出现了测试执行集群的架构；

再比如，当测试用例数量非常多，每次测试结束，需要分析所有失败的用例时，就必须要考虑基于机器学习的缺陷分类方法；

再比如，为了增加测试环境的有效使用时间，避免开发人员要在发生缺陷的环境上实时 Debug 而造成的测试环境占用问题，我们就会考虑在测试用例执行失败的时候自动获取全链路的日志；

再比如，如果我们的大量测试执行请求都是 API 调用，我们就可以实现完全基于 Docker 的并发执行环境。

除了上面这些技术驱动的原因外，还有些是由企业的组织结构驱动的。比如，eBay 的某些产品线，CI/CD 团队在印度，而测试架构团队在中国。大家都知道 CI/CD 的流水线脚本和测试执行是强耦合的，而要求两个异地团队实时合作并保证完全同步很困难。为此，我们就需要考虑为 CI/CD 提供统一的测试接口，这样流水线脚本就可以以解耦的方式与测试集成了。

问题六：你觉得在我给出的全球化电商的测试基础架构中，还可以增加哪些与测试相关的服务？

在专栏的第 42 篇文章《实战：大型全球化电商的测试基础架构设计》中，我根据自己的实践经验，把大型全球化电商网站全局测试基础架构的设计思路归纳为了“测试服务化”，并用一张图展示了整体的测试基础架构。

为了便于你回顾文章内容，我把文中用到的大型全球化电商网站的全局测试基础架构设计图，放到了这里。

其实，除了图中的各类测试服务以外，你完全可以根据你的业务场景来搭建更多的测试服务，以提高团队的测试效率和研发效能。

比如，你可以提供完整和详细被测系统所有版本信息的服务，通过一个简单的 Restful API 调用，就可以得到被测系统中所有组件、各个微服务的详细版本信息，还可以自动比较多个被测环境的版本差异。

再比如，为了解决微服务测试过程中的依赖性，你可以考虑提供基于消费者契约的统一 Mock 服务，以契约文件作为输入，来提供模拟真实服务的 Mock 服务，从而方便微服务的测试。

再比如，你还可以提供压力测试服务，由这个服务统一管理所有的 Load Generator 来发起测试，这个服务还可以进一步细分为前端性能测试服务，以及后端压力测试服务。

这样的例子还有很多，但在实际工作中，我们还是要根据测试本身的上下文以及被测产品的性质，来决定将哪些需求实现成测试服务会。

假如，你的项目只是偶尔会做前端性能测试，那么你就完全没必要去自己实现前端性能测试服务，直接采用现成的工具效率会更高。而如果你的产品需要经常做前端性能测试和优化，而且还不止一个团队会需要这种类型测试的时候，你就应该考虑将前端性能测试服务化了。

最后，感谢你能认真阅读第 35~42 这 8 篇文章的内容，并写下了你的想法和问题。期待你能继续关注我的专栏，继续留下你对文章内容的思考，我也在一直关注着你的留言、你的学习情况。

感谢你的支持，我们下一期答疑文章再见！

测试专栏特别放送 | 答疑解惑第四期

你好，我是茹炳晟。

今天的“答疑解惑”文章，我将针对 API 自动化测试和代码级测试这两个系列 6 篇文章中的问题，和你展开分享。

我还是会先简单概括下每篇文章的内容，并给出文章链接，帮助你复习相应的内容。同时，如果你再次阅读时还有哪些疑问的话，也欢迎你在文章下面继续留言。我会一直关注着你的学习情况，希望可以扫清软件测试精进道路上的障碍。

现在，我们就开始今天的主题吧。

问题一：实际项目中，往往会存在按时序的 API 调用以及异步 API 调用，这类 API 测试要如何开展？

在专栏的第 22 篇文章《从 0 到 1：API 测试怎么做？常用 API 测试工具简介》中，我以基于主流 Spring Boot 框架开发的简单 Restful API 为例，分别介绍如何使用 cURL 和 Postman 对其进行最基本的功能测试，希望可以让你先对 API 测试有个感性认识。另外，在这篇文章中，我还和你分享了目前一些常见的典型复杂场景，以及相应的测试思路和方法。

而在文章最后，我希望你思考的是实际项目中往往会存在按时序的 API 调用以及异步 API 调用，这类 API 测试要如何开展？现在，我来说说我的经验吧。

我们先一起看看按时序调用的 API 序列的测试场景。

对于此类测试，我一般建议通过 GUI 操作来录制 API 的调用。比如，在启用 Fiddler 的情况下，通过 GUI 来完成业务操作，随后去分析 Fiddler 抓取到的后端 API 请求顺序，然后以此来开发 API 测试用例。

开发测试用例的过程中还需要特别关注前后两个 API 调用之间的数据传递，比如需要将前一个 API 调用返回的 response 中的某个值作为参数传递给下一个 API 调用。

其次是异步 API 的测试。对于异步 API 测试的场景，我们往往先会采取“只验证其是否发起了正确的调用，而不直接验证操作结果”的方式。比如，你的被测 API 是一个异步操作的 API，那么我只会去验证这个 API 是否按照预期发起了正确的异步调用请求，而不会直接去验证异步操作的结果。如果这类测试全部通过后，我们才会考虑真正验证异步操作结果的测试用例。

举个实际的例子，假设你的被测 API A 完成的是下订单的操作。这个 API A 完成下订单操作要通过调用另外一个 API B 将订单信息写入到消息队列中去。而真正下订单成功指的是消息队列中的消息被后续服务正确处理并且成功了。此时，这里的后续消息处理就是异步的操作了。

那么，当我们要测试这个 API A 的时候，我们只需要验证它是否正确地发起了对 API B 的调用即可，而不用关心 API B 的具体行为结果。

也就是说，我们只关注 API A 是否以正确的参数调用了 API B 即可，而无需关注 API B 是否正确地执行了将订单信息写入消息队列的操作，更不用关注，消息队列中的消息被异步处理的结果。

注意这里的测试重点，应该更多地放在前面的部分，而真正验证异步操作结果的测试在资源有限的情况下只需覆盖最典型的场景即可。

问题二：对基于配置文件的 API 测试框架，你有哪些看法呢？

在专栏的第 23 篇文章《知其然知其所以然：聊聊 API 自动化测试框架的前世今生》中，我和你分享了 API 自动化测试框架的发展历程，帮助你理解 API 测试是如何一步一步地发展成今天的样子，希望可以以这种“知其所以然”的方式加深你对 API 自动化测试的理解。

而在这篇文章最后，我提到了基于配置文件的 API 测试框架，比如典型的 HttpRunner。在此类 API 测试框架的支持下，测试用例本身往往就是纯粹的配置文件了。如果你用过这个框架的话，我希望你可以谈谈你的看法。

对于基于配置文件的 API 测试框架的确是个不错的方向，尤其是国内的开源框架 HttpRunner 更是推动了这种测试框架的普及。

基于配置文件的 API 测试框架的优势，可以归纳为以下三方面：

降低了测试用例开发的门槛，使得完全没有代码基础的同学也可以很容易地完成 API 测试；

可以很方便地将 API 功能测试用例直接转换成 API 性能测试的用例（HttpRunner 可以使用 lucust 直接实现这样的转换）；

同时，HttpRunner 这类工具还支持直接从网络转发工具得到的 HAR 中提取 API 调用的测试用例，进一步降低了 API 测试用例的开发成本。

所以，基于配置文件的 API 测试框架很受初学者的欢迎。

但是，为了完成一些复杂场景的测试用例设计以及复杂的结果判断，你还是需要具备基本的代码能力，以完成这些复杂场景的测试实现。比如，HttpRunner 就会涉及到使用 debugtalk.py 来实现 hook 函数的功能扩展。

也就是说，完全不写代码的 API 测试只能覆盖大部分的简单测试场景，如果你要搞定复杂场景的 API 测试的话，你是要必须掌握一些基本的开发技能，这里没有任何捷径可走。

另外，很多读者的留言也很精彩，我这里特地选取了两条供大家参考。从 Cynthia 的留言中，我看得出她已经完全习得了这篇文章中描述的方法的精髓，这也正是我想要传达给你的最核心的内容。

Martin 在留言的后半部分中提到，通过 HttpRunner 来实现轻量级的 API 测试的确是个好方法，也最大程度地发挥了 HttpRunner 的价值。但是，留言前半部分的“Postman 转 Python 或者 Java”的观点我并不是很认同。其实，Postman 是有直接代码转换功能的，而且支持各种语言的各种框架，基本可以实现一键操作，所以，其实很多没有采用 HttpRunner 的企业都还在普遍使用这个方法。

问题三：如果无法通过 API Gateway 方法得到契约的话，应该采用什么方法来解决呢？

在专栏的第 24 篇文章《紧跟时代步伐：微服务模式下 API 测试要怎么做？》中，我和你分享了微服务模式下的 API 测试，旨在帮助你认清庞大的测试用例数量、微服务之间的相互耦合这两个问题的本质，以更好地开展测试。所以，我今天分享这个主题的目的就是，帮你理解这两个问题的本质，以及如何基于消费者契约的方法来解决这两个难题。

而在今天这篇文章最后，我希望你思考的是：基于消费者契约的 API 测试中，对于那些新开发的 API，或者加了新功能的 API，由于之前都没有实际的消费者，所以你无法通过 API Gateway 方法得到契约。对于这种情况，你会采用什么方法来解决呢？

从我的经验来看，因为缺乏契约，所以还是会采用传统的 API 测试方法，也就是根据 API 设计文档来设计测试用例。

这时，我们采取的 API 测试策略是：

对于已经上线的 API 我们会通过契约测试来保证质量；

而对于新的 API，或者是加了新功能的 API，则还是采用传统的基于 API 设计文档来设计测试用例，同时基于代码覆盖率来指导补充遗漏测试用例的方式来保证质量。当这些新 API 上线运行了一段时间后，我们就会缩小测试的范围，逐渐向契约测试过渡。

问题四：你所在公司，在进行代码级测试时，采用过哪些方法呢？

在专栏的第 25 篇文章《不破不立：掌握代码级测试的基本理念与方法》中，我根据实际工程项目中的实践，总结了五种常见的代码错误，以及对应的四大类代码级测试方法。这里我还想在多啰嗦一句，代码级测试的测试方法一定是一套测试方法的集合，而不是一个测试方法。

而在这篇文章最后，我希望你分享一下你所在公司，在进行代码级测试时采用过哪些方法，又是如何具体开展的。这里我来分享下我在 eBay 的经验吧。

在 eBay，代码质量保障已经完全纳入了 CI/CD 流水线。

首先，我们基于 Sonar 启用了静态代码。除了在上传 Git 的时候触发静态扫描外，开发人员在本地 IDE 中也会进行实时的静态扫描，并可以实时看到分析结果，这样就可以在代码被递交到代码仓库前就已经完成了预检测。

其实，我们并没有直接采用标准的代码静态扫描的规则库，而是删除了其中很多我们认为过于严格的规则，同时加入了一些我们认为比较重要的检测项，使得这个规则库更符合我们的业务场景。一般情况下，这些规则的修订是由测试架构师牵头，与开发主管和资深的开发人员一起协商决定的。

这里需要注意的是，我们并不要求静态扫描上报的所有错误都被修复后才能发布，只要求解决最关键的问题即可。而对于那些所谓的 Code Smell 问题，基于研发成本的考虑，我们并不会要求完全修复。

接着 CI/CD 流水线会触发代码动态测试，即单元测试。这里，我们不仅要求单元测试能够 100% 通过，并且会要求达到一定的代码覆盖率。在 eBay，我们对不同模块的代码覆盖率要求也不一样，并没有一个硬性指标。其实，这也是出于研发成本的考虑。

通常来讲，对底层模块以及提供公共服务的中间件的代码覆盖率指标的要求，一般都会比较高。而我们对前端模块的覆盖率要求，就会低很多。

问题五：除了 Sonar，你还用过哪些静态代码扫描工具，使用过程中遇到过哪些问题？

在专栏的第 26 篇文章《深入浅出之静态测试方法》中，我和你详细分享了人工静态测试方法和自动静态测试方法，来帮你理解研发流程上是如何保证代码质量的。另外，我以 Sonar 为例，和你分享了如何搭建自己的自动静态代码扫描方案，并且应用到项目的日常开发工作中去。

而在这篇文章最后，我希望你分享的是除了 Sonar 你还用过哪些静态代码扫描工具，使用过程中遇到过哪些问题。

其实优秀的代码静态扫描工具远远不止 Sonar，比如 Fortify SCA 和 Checkmarx CxSuite 等都是很优秀的静态扫描工具。至于使用过程中需要的问题，我觉得主要有这么三个：

第一是误报率。过高的误报率会降低开发人员对测试工具的信任度，而且还会引入很多人为标注的工作量。

第二是规则库的完备性和实用性。很多时候你会发现，标准代码规则库中的一些规则设计不够合理，有点教条主义。比如，有些规则库会强行规定一个函数的代码行数不能超过 200 行，从代码的模块化和易维护性角度来讲，过长的函数实现体的确不利于代码健康。但是，也不能完全一刀切，毕竟有些函数就是实现起来比较复杂。所以，很多时候我们需要对标准规则库进行深层次地裁剪，以更好地适应企业的实际情况。

第三是自定义规则的难易程度。虽然很多静态代码工具都提供了规则编辑器，来方便你实现自己的规则，但是这些规则编辑器的使用方法和语法的学习成本比较高，对初学者不够友好。

问题六：在单元测试过程中，你都遇到过哪些问题，又是如何解决的呢？

在专栏的第 27 篇文章《深入浅出之动态测试方法》中，我和你分享了人工动态测试方法和自动动态测试方法。因为自动动态方法并不能理解代码逻辑，所以仅仅被用于发现异常、崩溃和超时这类“有特征”的错误，而对于代码逻辑功能的测试，主要还是要依靠人工动态方法。

在这篇文章最后，我希望你分享的是，除了我在文中提到的几个单元测试的难点问题，你还遇到过哪些问题，又是如何解决的。

这里，我想再和你分享我曾在单元测试中遇到过的问题。

单元测试的难点中较为典型的就是对内部输入的控制。对于内部输入的控制有数十种不同的场景，这里我就举一个例子，意在抛砖引玉。

首先为了达到较高的测试代码覆盖率，如果代码中包括了 if-else 分支，那么我们的测试就需要分别执行到这两个分支。假设，现在有一个 if-else 分支是根据 malloc 这个内存分配函数的结果进行不同的处理，如果内存分配成功了，就执行 A 逻辑，如果执行失败了就执行 B 逻辑。

那么，在做单元测试的时候，通常情况下很容易覆盖内存分配成功的场景，但是想要实现“可控”的内存分配失败就比较困难了。因为 malloc 是个底层系统函数，根本无法对其控制。

为了解决这个问题，我们就可以采用桩函数的思想，引入一个 malloc 的桩函数，在这个桩函数的内部再去调用真正的系统 malloc 函数，如果需要模拟真正的 malloc 函数的失败，就在桩函数里面直接返回 malloc 函数失败的返回值，来达到模拟真正 malloc 函数失败场景的目的。这样就能在被测函数中通过可控的方式，来模拟系统底层函数的返回值了。

最后，感谢你能认真阅读第 22~27 这 6 篇文章的内容，并写下了你的想法和问题。期待你能继续关注我的专栏，继续留下你对文章内容的思考，我也在一直关注着你的留言、你的学习情况。

感谢你的支持，我们下一期答疑文章再见！

测一测 | 这些软件测试题目，你都掌握了吗？

经过将近五个月、60 篇文章的学习，相信你已经对软件测试的全貌有了一个大致的理解，而对某一种类型的测试（比如，GUI 自动化测试、移动 App 的自动化测试、性能测试等），也有了更深入的理解。

现在，我从专栏中精心挑选了 10 个核心知识点，组成了 10 道测试题目（包含 5 道选择题，5 道问答题）。希望可以帮助你检测一下这段时间的学习成果，以期对这些核心知识的理解可以达到炉火纯青的程度。

如果你刚刚打开这个专栏，可以用这 10 道题目，找到自己的薄弱点，对症下药；如果你已经学习了一段时间，可以用这 10 道题目，检测一下学习成果，查漏补缺。

我的建议是：你可以拿出纸笔，写下这 10 道题的答案，然后再与文末的答案进行对照。

选择题

1. （单选）当需要对某个系统进行测试的时候，应该从哪些方面来设计测试用例？

A. 功能验证

B. 性能相关的验证

C. 兼容性相关的验证

D. 安全性相关的验证

E. 以上全是

2. （多选）软件测试过程中，测试数据准备的痛点有哪些？（多选）

A. On-the-fly 测试数据准备的时间消耗

B. Out-of-box 测试数据的“脏数据”

C. 测试数据本身组合的复杂性和多样性

D. 性能测试数据准备的时间消耗

E. 微服务化后，跨多个微服务的数据准备缺乏完整的知识体系

F. 微服务化后，测试数据准备的环境依赖性

3. （单选）无头浏览器的主要应用场景是？

A. 网络爬虫

B. GUI 自动化功能测试

C. 页面监控

D. 以上全是

4. （单选）以下不属于 API 测试工具的是哪个？

A. Postman

B. SoapUI

C. JMeter

D. Selenium

5. （单选）以下属于移动应用测试的工具是哪个？

A. Appium

B. UFT

C. TestNG

D. LoadRunner

问答题

GUI 自动化测试脚本分层设计的最佳实践是怎么样？

多个 API 连续调用的测试用例的难点是什么？你是如何来解决的？

单元测试中，桩函数和 Mock 函数用来解决什么问题，两者又有什么区别？

性能压测过程中，当面对大量并发用户调用的时候，服务器端 CPU 的使用率是高好还是低好？为什么？

当需要在尽可能短的时间内完成大量 GUI 自动化测试用例的执行时，业界主流的解决方案是什么？

答案与解析

1. （单选）答案：E

解析：在专栏第 1 篇文章《你真的懂测试吗？从“用户登录”测试谈起》中，我和你分享了设计一个测试用例，除了要考虑显示的功能性需求外，还要涉及安全性、性能、兼容性等非功能性需求的验证。

2. （多选）答案：ABCDEF

解析：在专栏的第 15 篇文章《过不了的坎：聊聊 GUI 自动化过程中的测试数据》、第 36 篇文章《浅谈测试数据的痛点》中，我从测试时机准备的角度，和你分享了测试数据准备有哪些痛点。

而关于现在流行的微服务模式，由于每个单一功能的服务都是独立分开部署的，所以我们在准备测试数据时，还可能会遇到诸如环境依赖、跨多个微服务的数据准备缺乏完整的知识体系等问题。

3. （单选）答案：D

解析：我在专栏的第 16 篇文章《脑洞大开：GUI 测试还能这么玩（Page Code Gen + Data Gen + Headless）？》中，和你分享过：无头浏览器的主要应用场景，包括 GUI 自动化测试、页面监控以及网络爬虫这三种。

4. （单选）答案：D

解析：Selenium 属于 GUI 自动化测试工具。我还在第 12 篇文章《从 0 到 1：你的第一个 GUI 自动化测试》中，基于 Selenium 和你一起搭建了我们的第一个测试用例，你还记得吗？

5. （单选）答案：A

解析：UFT（以前的 QTP）属于一款 GUI 测试工具，LoadRunner 属于性能测试工具。而 TestNG 是一个用来简化广泛的测试需求的测试框架，适用于从单元测试到集成测试阶段的测试。

Appium 则是一款很好用的移动测试工具。如果你不记得它的使用方法了，可以再回顾下第 21 篇文章《移动测试神器：带你玩转 Appium》中的内容。

6. GUI 自动化测试脚本分层设计的最佳实践是怎样的？

考点分析：GUI 自动化测试脚本的分层设计原理。

答案与解析：

大量 GUI 自动化测试能够成功的关键，就在于脚本的分层设计。而脚本分层设计的核心思想就是模块化。

首先，我们需要对页面进行抽象，形成页面对象模型。在这样的测试用例中，你看到的都是类似于 XXXPage.YYYComponent.ZZZOperation 的语句。它们和实际的手工测试可以建立一一对应的关系，用通俗的话语来讲，就是某某页面上的某某元素，执行了某某操作。

接下来，为了使 GUI 自动化测试脚本更加符合业务场景的描述，同时进一步提高脚本的封装性和可重用性，就需要引入业务流程脚本的概念。这里，业务流程和实际的业务流程也是一一对应的关系。这样，测试用例就可以通过调用业务流程脚本来实现，测试用例本身的可读性以及可维护性也会更好。同样地，业务流程脚本，也是基于页面对象模型实现的。

关于页面对象模型的细节，你可以再回顾下第 13 篇文章《效率为王：脚本与数据的解耦 + Page Object 模型》中的相关内容。

而关于业务流程抽象的细节，你可以再回顾下第 14 篇文章《更接近业务的抽象：让自动化测试脚本更好地描述业务》中的相关内容。

7. 多个 API 连续调用的测试用例设计难点是什么？你是如何解决的？

考点分析：多个 API 连续调用时，前后两个 API 之间的参数传递。

答案与解析：

单个 API 测试并不难，难的是多个 API 的连续调用，并且后一个 API 的参数值使用的是前一个 API 调用的返回结果，这就要求多个 API 调用之间可以方便地进行参数传递。一个最典型的场景就是，前一个 API 调用会返回一个有效的 token，后一个 API 调用需要带着这个 token 才能调用成功。

为了解决这个问题，一般来讲有三种处理方法：

第一种方法是，手工复制前一个 API 返回结果中的某个值，然后粘贴给后一个 API 作为输入参数。当然，这是最基本的方法，但是效率太低，而且无法实现自动化。

第二种方法是，使用基于代码的 API 测试框架。由于此时所有的测试逻辑都是通过代码来实现的，因此可以很容易地实现 API 之间的参数传递。

第三种方法是，借助于类似 HttpRunner 之类的已有 API 测试框架。此类框架可以通过关键字，很方便地将前一个 API 的返回值中的某个值传递给下一个 API 作为输入参数。

关于复杂场景下的 API 测试，建议你再回顾下第 22 篇文章《从 0 到 1：API 测试怎么做？常用 API 测试工具简介》中的相关内容，以及《测试专栏特别放送 | 答疑解惑第四期》中的第一个问题。毕竟，复杂场景的 API 测试，才是我们业务场景中最常遇到的，也是软件测试工程师面试过程中经常会被问题到的问题。

8. 单元测试中，桩函数和 Mock 函数主要用来解决什么问题？这两者又有什么区别呢？

考点分析：理解桩函数和 Mock 函数的本质区别。

答案与解析：

当被测函数中调用了第三方的函数时，我们一般会采用桩函数或者 Mock 函数来模拟这些第三方函数，以此来实现被测函数的高代码覆盖率。可以说，桩函数和 Mock 函数的使用大大方便了单元测试的开展，同时也解决了单元测试的代码耦合性问题。

但是，这两者到底有什么区别呢？

通俗来讲，如果你的测试验证是在被测函数中进行的，那么此时你使用的就是桩函数；而如果你的测试验证是在被模拟的函数中进行的，那么这个被模拟的函数就是 Mock 函数。

更详细的分析，你可以再回看下第 3 篇文章《什么是单元测试？如何做好单元测试？》中的相关内容。

9. 性能压测过程中，当面对大量并发用户调用的时候，服务器端 CPU 的使用率是高好还是低好？为什么？

考点分析：理解性能测试指标解读的复杂性，必须要全盘考虑多个指标间的相互关联和制约。

答案与解析：

这个问题的答案，一定会有坚持不同意见的两派人。

一部分人认为，CPU 使用率当然是越低越好。这说明后端代码实现得很高效，只占用很少的计算资源就能实现较高的并发。并发情况下，越低的 CPU 占用率，说明系统可以继续承载越多的并发负载。

而另一部分人则认为，CPU 的使用率是越高越好。这说明系统的计算资源被充分利用了起来。

你同意哪个观点呢？

其实，这个问题本身就是个伪命题，单单通过题干中的信息是不足以给出孰好孰坏的结论的。这里的关键是，随着并发用户数的上升，事务的响应时间是如何变化的。

如果随着并发用户数的增加，事务的响应时间也呈线性增长，但 CPU 的使用率一直上不去，这就是典型的 CPU 资源没有被充分利用的现象。此时，你就需要去进一步诊断为什么 CPU 资源不能在并发场景下被充分利用。

而如果随着并发用户数的增加，事务的响应时间能基本保持稳定，同时 CPU 的使用率会随着并发用户数的增加呈线性增加，这反倒是我们希望看到的结果，也就是说更多的并发用户会需要使用更多的 CPU 资源。

10. 当需要在尽可能短的时间内，执行完大量 GUI 自动化测试用例时，业界主流的解决方案是什么？

考点分析：测试执行架构的设计

答案与解析：

这个问题其实不难回答，业界一般会采用两种方案：

一种是，使用第三方的云测服务，比如国外的 Sauce Labs、国内的 Testin 等；

另一种是，自己搭建 Selenium Grid 集群。

其实，这两种方案的本质都是将大量的测试用例以并发的方式来执行。更多的技术细节，你可以参考第 39 篇文章《从小作坊到工厂：什么是 Selenium Grid？如何搭建 Selenium Grid？》中的相关内容。

结束语 | 不是结束，而是开始

你好，我是茹炳晟。

不知不觉间，我们已经一起走过了将近 5 个月的时间。

在这 5 个月的时间里，我和你分享了软件测试的方方面面，小到一个“用户登录”功能测试用例的设计，大到从网站架构设计的视角去审视测试设计，相信我已经帮你建立起了软件测试工程领域的全局观。

同时，也正是你的支持与认可，在这 5 个月的时间里，一直激励着我，不断地总结自己从事软件测试工作这些年来的收获与成长，并形成文字，分享给你，以惠及更多的软件测试行业的同伴。

这里，我也想再和你分享下，我在专栏创作过程中的一些心路历程，希望你也能看到我在这 5 个月时间里的思考与成长。

首先，专栏写作的过程真的很辛苦。从今年 4 月初接受极客时间邀请，到今天专栏结束，这中间 7 个多月的时间，26 万字的专栏写作，几乎占尽了我所有的个人时间，以至于家人误会我改行做了“作家”，天天晚上躲在书房里写“武林秘籍”。

其次，专栏定位也真的是我冥思苦想许久，才最终得以确定。即使这样，在专栏更新的过程中，我也一度因为“你”的留言，而倍感困惑。因为，8000 多个订阅用户的知识储备和诉求各有不同。所以，你现在看到的每篇文章中的知识点、案例，我都进行了谨慎细致地考量和打磨。

这其中我考虑到了文章的难易程度，以求可以让处于各个成长阶段的“你”都能有所收获，还要尽量保证知识结构的完整性，以保证可以为你描绘出软件测试领域的全景技术视图。同时，每一个知识点，我都尽量避免去讲解具体某一款工具的使用，希望做到有点有面，能够给你呈现全方位的测试视角。

但是，不管这个过程如何的艰辛和痛苦，我写作专栏的决心却一直未变。因为，我希望，通过这个专栏我可以帮助到无数处于迷茫阶段的测试技术人员，帮助他们明确定位、认识不足、建立信心，并为他们指明方向。

虽然现在市场上已经有很多软件测试类的书籍了，但是大部分都是专门讲解具体测试工具的使用，而且对原理的讲述并没有那么透彻。另外，在我看来，市面上一直缺少一份从工程实践角度出发，尤其是从互联网的实际工程实践出发，来全面介绍软件测试技术的学习资料。为此，我希望这个专栏能够填补这一空缺。

现在专栏已经进入了尾声，于我而言是一个创作过程的结束，但于你而言这只是开始。

其实，这短短的 61 篇文章，更多的是帮你开阔视野，感受到软件测试庞大的知识体系，并了解到原来其中的每个领域（比如性能测试、安全测试、自动化测试等），都是一个很大的分支。

而我把你带进了软件测试世界的大门后，后面更多的探索与修炼就全要靠你自己来完成了。这就好比金庸先生笔下的武侠世界，专栏本身更像是一本武林秘籍，而想学得一身本领行走江湖，还得要靠你自己的修炼。

所以，接下来，我想再和你多啰嗦几句，用什么样的“姿势”去修炼，才能事半功倍。

第一，苦练基本功。我在专栏文章中，提到了很多测试工具，但是都没有对这个工具本身的使用做详细展开，这部分内容需要你自己去学习并在项目中实践。这么设计的很大一部分原因在于，对工具的学习我始终推荐直接参考官方文档。熟练掌握这些工具，是成为武林高手的敲门砖。这相当于是你要重点练习的基本招式，没有人可以代替你完成。

第二，行走江湖。我在专栏文章中，和你分享了很多来自于大公司的工程实践方法，你应该根据你所在公司或者项目的上下文，来综合考虑如何实践。这里你一定不要全盘照抄，一定需要根据实际情况进行取舍，甚至需要你做到融会贯通、综合应用这些实践。这是成为武林高手必定要经历的过程，讲究的是此时无招胜有招。

第三，自立门派。我还在专栏文章中，和你分享了很多测试相关的创新设计，比如测试数据服务、基于 Docker 的测试执行环境、页面对象的自动生成、API 返回结果的自动比较等等，这些都是工程实践沉淀的精华内容。虽然很多技术实现并没有开源，但是这些设计思想和方法都是可以借鉴的，可以帮助你大幅度提升测试效率。这里，你要做的就是根据这些设计思路，然后结合你所在企业的技术栈，来落地实现这些方法。这就是打造你的测试核心竞争力，对应的就是修炼你的独门绝活，在武林中自立门户。

在专栏开始时，我曾在开篇词中和你说到“希望四个月后，你我都能遇见更好的自己”。通过这几个月的专栏创作，我是真的遇见了更好的自己，那么你呢？