软件测试学习笔记

软件测试不仅需要从业务本身出发来对软件进行手工测试验证,还需要掌握完整的自动化测试开发技术来设计自动化测试用例。作为一名软件测试工程师,必须掌握设计开发测试基础架构的关键技术,系统性地思考如何才能将测试数据的准备工具化,服务化,最终实现平台化。

基础知识

软件测试基础

一个良好的测试用例应包含三种方法:等价类划分,边界值分析,错误推测。
  • 等价类划分方法,是将所有可能的输入数据划分成若干个子集,在每个子集中,如果任意一个输入数据对于揭露程序中潜在错误都具有同等效果,那么这样的子集就构成了一个等价类。后续只要从每个等价类中任意选取一个值进行测试,就可以用少量具有代表性的测试输入取得较好的测试覆盖结果。
  • 边界值分析方法,是选取输入、输出的边界值进行测试。因为通常大量的软件错误是发生在输入或输出范围的边界上,所以需要对边界值进行重点测试,通常选取正好等于、刚刚大于或刚刚小于边界的值作为测试数据。
  • 错误推测法,是指基于对被测试软件系统设计的理解、过往经验以及个人直觉,推测出软件可能存在的缺陷,从而有针对性地设计测试用例的方法。这个方法强调的是对被测试软件的需求理解以及设计实现的细节把握,当然还有个人的能力。
等价类划分法和边界值分析法是两种常见的黑盒测试方法。从方法论上可以看出来,边界值分析是对等价类划分的补充,所以这两种测试方法经常结合起来使用。
以“用户登录”功能为例,基于等价类划分和边界值分析方法,常见的测试用例包括
  1. 输入已注册的用户名和正确的密码,验证是否登录成功;
  2. 输入已注册的用户名和不正确的密码,验证是否登录失败,并且提示信息正确;
  3. 输入未注册的用户名和任意密码,验证是否登录失败,并且提示信息正确;
  4. 用户名和密码两者都为空,验证是否登录失败,并且提示信息正确;
  5. 用户名和密码两者之一为空,验证是否登录失败,并且提示信息正确;
  6. 如果登录功能启用了验证码功能,在用户名和密码正确的前提下,输入正确的验证码,验证是否登录成功;
  7. 如果登录功能启用了验证码功能,输入错误的验证码,验证是否登录失败,并且提示信息正确。
此外,还可以增加的测试用例有
  1. 用户名和密码是否大小写敏感;
  2. 页面上的密码框是否加密显示;
  3. 随机生成用户首次登录成功时,是否提示修改密码;
  4. 忘记用户名和忘记密码的功能是否可用;
  5. 用户名和密码长度是否在前端做限制;
  6. 如果登录功能需要验证码,点击验证码图片是否可以更换验证码,更换后的验证码是否可用;
  7. 刷新页面是否会刷新验证码;
  8. 如果验证码具有时效性,需要分别验证时效内和时效外验证码的有效性;
  9. 用户登录成功但是会话超时后,继续操作是否会重定向到用户登录界面;
  10. 不同级别的用户,比如管理员用户和普通用户,登录系统后的权限是否正确;
  11. 页面默认焦点是否定位在用户名的输入框中;
  12. 快捷键 Tab 和 Enter 等,是否可以正常使用。
软件分为显式功能性需求和非功能性需求。以上的测试目标全都是属于典型的功能性需求,而非功能性需求则应涵盖安全性,性能及兼容性三大方面。
安全性测试用例包括
  1. 用户密码后台存储是否加密;
  2. 用户密码在网络传输过程中是否加密;
  3. 密码是否具有有效期,到期后是否提示修改密码;
  4. 不登录的情况下,在浏览器中直接输入登录后的 URL 地址,验证是否会重新定向到用户登录界面;
  5. 密码输入框是否不支持复制和粘贴;
  6. 密码输入框内输入的密码是否都可以在页面源码模式下被查看;
  7. 用户名和密码的输入框中分别输入典型的“SQL 注入攻击”字符串,验证系统的返回页面;
  8. 用户名和密码的输入框中分别输入典型的“XSS 攻击”字符串,验证系统行为是否被篡改;
  9. 连续多次登录失败情况下,系统是否会阻止后续的尝试以应对暴力破解;
  10. 同一用户在同一终端的多种浏览器上登录,验证登录功能的互斥性是否符合设计预期;
  11. 同一用户先后在多台终端的浏览器上登录,验证登录是否具有互斥性。
性能压力测试用例包括
  1. 单用户登录的响应时间是否小于 3 秒;
  2. 单用户登录时,后台请求数量是否过多;
  3. 高并发场景下用户登录的响应时间是否小于 5 秒;
  4. 高并发场景下服务端的监控指标是否符合预期;
  5. 高集合点并发场景下,是否存在资源死锁和不合理的资源等待;
  6. 长时间大量用户连续登录和登出,服务器端是否存在内存泄漏。
兼容性测试用例包括
  1. 不同浏览器下,验证登录页面的显示以及功能正确性;
  2. 相同浏览器的不同版本下,验证登录页面的显示以及功能正确性;
  3. 不同移动设备终端的不同浏览器下,验证登录页面的显示以及功能正确性;
  4. 不同分辨率的界面下,验证登录页面的显示以及功能正确性。
所谓的“穷尽测试”是指包含了软件输入值和前提条件所有可能组合的测试方法,完成穷尽测试的系统里应该不残留任何未知的软件缺陷。在绝大多数的软件工程实践中,测试由于受限于时间成本和经济成本,是不可能去穷尽所有可能的组合。采用基于风险驱动的模式,有所侧重地选择测试范围和设计测试用例,以寻求缺陷风险和研发成本之间的平衡,才是测试的关键。

单元测试

单元测试通常由开发工程师完成,一般会伴随开发代码一起递交至代码库。单元测试属于最严格的软件测试手段,是最接近代码底层实现的验证手段,可以在软件开发的早期以最小的成本保证局部代码的质量。
单元测试的实施过程还可以帮助开发工程师改善代码的设计与实现,并能在单元测试代码里提供函数的使用示例,因为单元测试的具体表现形式就是对函数以各种不同输入参数组合进行调用,这些调用方法构成了函数的使用说明。
做好单元测试,需要从以下三个方面入手:
  1. 代码的基本特征与产生错误的原因。要做到代码功能逻辑正确,必须做到分类正确并且完备无遗漏,同时每个分类的处理逻辑必须正确。
  2. 单元测试用例详解。单元测试的用例是一个“输入数据”和“预计输出”的集合。需要针对确定的输入,根据逻辑功能推算出预期正确的输出,并且以执行被测试代码的方式进行验证。
  3. 驱动代码,桩代码和 Mock 代码。驱动代码(Driver)指调用被测函数的代码,桩代码(Stub)和 Mooc 代码是用来代替真实代码的临时代码。
编写桩代码需要遵守以下三个原则:
  1. 桩函数要具有与原函数完全相同的原形,仅仅是内部实现不同,这样测试代码才能正确链接到桩函数;
  2. 用于实现隔离和补齐的桩函数比较简单,只需保持原函数的声明,加一个空的实现,目的是通过编译链接;
  3. 实现控制功能的桩函数是应用最广泛的,要根据测试用例的需要,输出合适的数据作为被测函数的内部输入。
实际项目中开展单元测试,需要注意四个方面:
  1. 并不是所有的代码都要进行单元测试,通常只有底层模块或者核心模块的测试中才会采用单元测试。
  2. 单元测试框架的选型和开发语言直接相关。
  3. 引入计算代码覆盖率的工具。
  4. 把单元测试执行、代码覆盖率统计和持续集成流水线做集成,以确保每次代码递交,都会自动触发单元测试,并在单元测试执行过程中自动统计代码覆盖率。

自动化测试

常见的自动化测试技术有单元测试的自动化技术,代码级集成测试的自动化技术,API 测试的自动化技术,GUI 测试的自动化技术。
自动化测试的本质是先写一段代码,然后去测试另一段代码,所以实现自动化测试用例本身属于开发工作,需要投入大量的时间和精力,并且已经开发完成的用例还必须随着被测对象的改变而不断更新,还需要为此付出维护测试用例的成本。当你发现自动化测试用例的维护成本高于其节省的测试成本时,自动化测试就失去了价值与意义,你也就需要在是否使用自动化测试上权衡取舍了。
此外,自动化测试还有一些常见的注意事项:
  1. 自动化测试并不能取代手工测试,它只能替代手工测试中执行频率高、机械化的重复步骤。
  2. 自动测试远比手动测试脆弱,无法应对被测系统的变化。
  3. 只有当开发完成的测试用例的有效执行次数大于等于 5 次时,才能收回自动化测试的开发成本。
  4. 手工测试发现的缺陷数量通常比自动化测试要更多,并且自动化测试仅仅能发现回归测试范围的缺陷。
  5. 测试的效率很大程度上依赖自动化测试用例的设计以及实现质量,不稳定的自动化测试用例实现比没有自动化更糟糕。
  6. 实行自动化测试的初期,用例开发效率通常都很低,大量初期开发的用例通常会在整个自动化测试体系成熟,和测试工程师全面掌握测试工具后,需要重构。
  7. 业务测试专家和自动化测试专家通常是两批人,前者懂业务不懂自动化技术,后者懂自动化技术但不懂业务,只有二者紧密合作,才能高效开展自动化测试。
适合自动化测试的项目,都具有以下特征:
  1. 需求稳定,不频繁变更
  2. 研发和维护周期长,需要频繁执行回归测试
  3. 需要在多种平台上重复运行相同测试的场景
  4. 某些测试项目通过手工测试无法实现,或者手工成本太高
  5. 被测软件的开发较为规范,能够保证系统的可测试性
  6. 预留可测试性接口

覆盖率测试

需求覆盖率是指测试对需求的覆盖程度,通常的做法是将每一条分解后的软件需求和对应的测试建立一对多的映射关系,最终目标是保证测试可以覆盖每个需求,以保证软件产品的质量。覆盖率,又分为需求覆盖率和代码覆盖率两个部分。
  1. 需求覆盖率是指测试对需求的覆盖程度,通常的做法是将每一条分解后的软件需求和对应的测试建立一对多的映射关系,最终目标是保证测试可以覆盖每个需求,以保证软件产品的质量。软件需求转换成测试需求,然后基于测试需求再来设计测试点。
    统计代码覆盖率的根本目的是找出潜在的遗漏测试用例,并有针对性的进行补充,同时还可以识别出代码中那些由于需求变更等原因造成的不可达的废弃代码。
  2. 代码覆盖率是指至少被执行了一次的条目数占整个条目数的百分比。最常用的三种代码覆盖率指标有行覆盖率,判断覆盖率(分支覆盖率)和条件覆盖率(结果覆盖率)。
    • 行覆盖率又称为语句覆盖率,指已经被执行到的语句占总可执行语句(不包含类似 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 各一次。
实现代码覆盖率的统计,最基本的方法就是注入(Instrumentation)。简单地说,注入就是在被测代码中自动插入用于覆盖率统计的探针(Probe)代码,并保证插入的探针代码不会给原代码带来任何影响。
对于 Java 代码来讲,根据注入目标的不同,可以分为源代码(Source Code)注入和字节码(Byte Code)注入两大类。基于 JVM 本身特性以及执行效率的原因,目前主流的工具基本都是使用字节码注入,注入的具体实现采用 ASM 技术。ASM 是一个 Java 字节码操纵框架,能被用来动态生成类或者增强既有类的功能,可以直接产生 class 文件,也可以在类被加载入 JVM 之前动态改变类行为。
根据注入发生的时间点,字节码注入又可以分为两大模式:On-The-Fly 注入模式和 Offline 注入模式。
  • On-The-Fly
  1. 开发自定义的类装载器(Class Loader)实现类装载策略,每次类加载前,需要在 class 文件中插入探针,早期的 Emma 就是使用这种方案实现的探针插入;
  2. 借助 Java Agent,利用执行在 main() 方法之前的拦截器方法 premain() 来插入探针,实际使用过程中需要在 JVM 的启动参数中添加“-javaagent”并指定用于实时字节码注入的代理程序,这样代理程序在装载每个 class 文件前,先判断是否已经插入了探针,如果没有则需要将探针插入 class 文件中,目前主流的 JaCoCo 就是使用了这个方式。
  • Offline
    Offline 模式也无需修改源代码,但是需要在测试开始之前先对文件进行插桩,并事先生成插过桩的 class 文件。它适用于不支持 Java Agent 的运行环境,以及无法使用自定义类装载器的场景。
    根据是生成新的 class 文件还是直接修改原 class 文件,又可以分为 Replace 和 Inject 两种不同模式。和 On-The-Fly 注入模式不同,Replace 和 Inject 的实现是,在测试运行前就已经通过 ASM 将探针插入了 class 文件,而在测试的运行过程中不需要任何额外的处理。Cobertura 就是使用 Offline 模式的典型代表。

软件缺陷报告

缺陷报告是测试工程师与开发工程师交流沟通的重要桥梁,也是测试工程师日常工作的重要输出。好的缺陷报告绝对不是大量信息的堆叠,而是以高效的方式提供准确有用的信息。
缺陷报告,一般由权限标题,缺陷概述,缺陷影响,配置环境,前置条件,缺陷重现步骤,期望结果和实际结果,优先级和严重程度,变通方案,根本原因分析,附件一共 11 个部分组成。
  • 缺陷标题通常是别人最先看到的部分,是对缺陷的概括性描述,通常采用“在什么情况下发生了什么问题”的模式。
  • 缺陷影响描述的是,缺陷引起的问题对用户或者对业务的影响范围以及严重程度。缺陷影响决定了缺陷的优先级(Priority)和严重程度(Severity)。
  • 环境配置用以详细描述测试环境的配置细节,为缺陷的重现提供必要的环境信息。
  • 前置条件是指测试步骤开始前系统应该处在的状态,其目的是减少缺陷重现步骤的描述。合理地使用前置条件可以在描述缺陷重现步骤时排除不必要的干扰,使其更有针对性。
  • 缺陷重现步骤是整个缺陷报告中最核心的内容,其目的在于用简洁的语言向开发工程师展示缺陷重现的具体操作步骤。操作步骤通常是从用户角度出发来描述的,每个步骤都应该是可操作并且是连贯的,所以往往会采用步骤列表的表现形式。
  • 期望结果需要说明应该发生什么,而不是什么不应该发生;而描述实际结果时,你应该说明发生了什么,而不是什么没有发生。
  • 严重程度是缺陷本身的属性,通常确定后就不再变化,而优先级是缺陷的工程属性,会随着项目进度、解决缺陷的成本等因素而变动
  • 变通方案是提供一种临时绕开当前缺陷而不影响产品功能的方式,变通方案的有无以及实施的难易程度,是决定缺陷优先级和严重程度的重要依据。
  • 根本原因分析就是我们平时常说的 RCA,从代码的角度发现问题,定位出问题的根本原因,清楚地描述缺陷产生的原因并反馈给开发工程师
  • 附件通常是为缺陷的存在提供必要的证据支持,常见的附件有界面截图、测试用例日志、服务器端日志、GUI 测试的执行视频等。

测试计划

测试计划包括了测试范围,测试策略,测试资源,测试进度,测试风险预估五大方面,需要以迭代的方式持续制定。
  1. 测试范围需明确某次测试中,“测什么”和“不测什么”的区间。
  2. 测试策略会要求我们明确测试的重点,以及各项测试的先后顺序。这些顺序包括了功能测试,兼容性测试和性能测试三个大方面及接口测试、集成测试、安全测试、容量验证、安装测试、故障恢复测试等几个小方面。
  3. 测试资源需要明确“谁来测”和“在哪里测”这两个问题。这两个问题换一个角度来衡量,就是判断测试工程师的能力和测试平台性能的标准。
  4. 测试进度主要描述各类测试的开始时间,所需工作量,预计完成时间,并以此为依据来建议最终产品的上线发布时间。行为驱动开发,指的是可以通过自然语言书写非程序员可读的测试用例,并通过 StepDef 来关联基于自然语言的步骤描述和具体的业务操作实现测试进度的辨别度。
  5. 测试风险预估适用于敏捷开发。制定测试计划时,你就要预估整个测试过程中可能存在的潜在风险,包括需求变更、开发延期、发现重大缺陷和人员变动,以及当这些风险发生时的应对策略

GUI 自动化测试

Selenium

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

Selenium 1.0 实现原理

Selenium 1.0,又称 Selenium RC。原理是:JavaScript 代码可以很方便地获取页面上的任何元素并执行各种操作。但是因为”同源政策(Same-origin policy)”(只有来自相同域名、端口和协议的 JavaScript 代码才能被浏览器执行),所以要想在测试用例运行中的浏览器中,注入 JavaScript 代码从而实现自动化的 Web 操作,Selenium RC 就必须“欺骗”被测站点,让它误以为被注入的代码是同源的。
Selenium RC 主要由 Remote Control Server 和 Client Libraries 两部分组成。Selenium RC Server,主要包括 Selenium Core,Http Proxy 和 Launcher 三部分:
  • Selenium Core,是被注入到浏览器页面中的 JavaScript 函数集合,用来实现界面元素的识别和操作;
  • Http Proxy,作为代理服务器修改 JavaScript 的源,以达到“欺骗”被测站点的目的;
  • Launcher,用来在启动测试浏览器时完成 Selenium Core 的注入和浏览器代理的设置;

Selenium 2.0 实现原理

Selenium 2.0,又称 Selenium WebDriver,它利用的原理是:使用浏览器原生的 WebDriver 实现页面操作。
  1. 当使用 Selenium2.0 启动浏览器 Web Browser 时,后台会同时启动基于 WebDriver Wire 协议的 Web Service 作为 Selenium 的 Remote Server,并将其与浏览器绑定。绑定完成后,Remote Server 就开始监听 Client 端的操作请求。
  2. 执行测试时,测试用例会作为 Client 端,将需要执行的页面操作请求以 Http Request 的方式发送给 Remote Server。该 HTTP Request 的 body,是以 WebDriver Wire 协议规定的 JSON 格式来描述需要浏览器执行的具体操作。
  3. Remote Server 接收到请求后,会对请求进行解析,并将解析结果发给 WebDriver,由 WebDriver 实际执行浏览器的操作。
  4. WebDriver 可以看做是直接操作浏览器的原生组件(Native Component),所以搭建测试环境时,通常都需要先下载浏览器对应的 WebDriver

数据驱动测试

如果在测试脚本中硬编码(hardcode)测试数据的话,测试脚本灵活性会非常低。而且,对于那些具有相同页面操作,而只是测试输入数据不同的用例来说,就会存在大量重复的代码。
这时候,就应该引入数据驱动测试。把测试数据和测试脚本分离。也就是说测试脚本只有一份,其中需要输入数据的地方会用变量来代替,然后把测试输入数据单独放在一个文件中。这个存放测试输入数据的文件,通常是表格的形式,也就是最常见的 CSV 文件。然后,在测试脚本中通过 data provider 去 CSV 文件中读取一行数据,赋值给相应的变量,执行测试用例。接着再去 CSV 文件中读取下一行数据,读取完所有的数据后,测试结束。CSV 文件中有几行数据,测试用例就会被执行几次。
  • 数据驱动很好地解决了大量重复脚本的问题,实现了“测试脚本和数据的解耦”。
  • 数据驱动测试的数据文件中不仅可以包含测试输入数据,还可以包含测试验证结果数据,甚至可以包含测试逻辑分支的控制变量。
  • 数据驱动测试的思想不仅适用于 GUI 测试,还可以用于 API 测试、接口测试、单元测试等。
创建测试数据从创建的技术手段上来讲,创建测试数据的方法主要分为两种:
  1. 测试用例执行过程中,实时创建测试数据,我们通常称这种方式为 On-the-fly。
  2. 测试用例执行前,事先创建好“开箱即用”的测试数据,我们通常称这种方式为 Out-of-box。
从理论上讲,On-the-fly 是很好的方法,但在实际测试项目中却并不是那么回事儿,往往会存在三个问题:
  1. 在用例执行过程中实时创建数据,导致测试的执行时间比较长。
  2. 业务数据的连带关系,导致测试数据的创建效率非常低。
  3. 实时创建测试数据的方式对测试环境的依赖性很强。
Out-of-box 的方式有效解决了 On-the-fly 的很多问题,但是这种方法的缺点也很明显,主要体现在以下三个方面:
  1. 测试用例中需要硬编码(hardcode)测试数据,额外引入了测试数据和用例之间的依赖。
  2. 只能被一次性使用的测试数据不适合 Out-of-box 的方式。
  3. “预埋”的测试数据的可靠性远不如实时创建的数据。
基于 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 自动化脚本直接去调用这些操作函数来构成整个测试用例,这样 GUI 自动化测试脚本就从原本的“流水账”过渡到了“可重用脚本片段”
页面对象模型的核心理念是,以页面(Web Page 或者 Native App Page)为单位来封装页面上的控件以及控件的部分操作。而测试用例,更确切地说是操作函数,基于页面封装对象来完成具体的界面操作,最典型的模式是“XXXPage.YYYComponent.ZZZOperation”。

测试脚本描述业务

操作函数的粒度是指,一个操作函数到底应该包含多少操作步骤才是最合适的。脚本粒度的控制还是有设计依据可以遵循的,即往往以完成一个业务流程(business flow)为主线,抽象出其中的“高内聚低耦合”的操作步骤集合,操作函数就由这些操作步骤集合构成。
  • 如果粒度太大,就会降低操作函数的可重用性。
  • 如果粒度太小,也就失去了操作函数封装的意义。
  • 每个测试工程师对操作函数的粒度理解也不完全相同,很有可能出现同一个项目中脚本粒度差异过大,以及某些操作函数的可重用性低的问题。
业务流程抽象,即用于解决如何把控操作函数的粒度,基于操作函数的更接近于实际业务的更高层次的抽象方式。基于业务流程抽象实现的测试用例往往灵活性会非常好,你可以很方便地组装出各种测试用例。
对于每一个业务流程类,都会有相应的业务流程输入参数类与之一一对应。具体的步骤通常有这么几步:
  1. 初始化一个业务流程输入参数类的实例;
  2. 给这个实例赋值;
  3. 用这个输入参数实例来初始化业务流程类的实例;
  4. 执行这个业务流程实例。
执行业务流程实例的过程,其实就是调用操作函数来完成具体的页面对象操作的过程。由于更接近实际业务,所以可以很方便地和 BDD 结合。BDD 就是 Behavior Driven Development,即行为驱动开发,业务流程(Business Flow)的封装更接近实际业务。基于业务流程的测试用例非常标准化,遵循“参数准备”、“实例化 Flow”和“执行 Flow”这三个大步骤,非常适用于测试代码的自动生成。

GUI 自动化测试报告

基于视频

为了分析测试用例的执行过程与结果,早期就出现了基于视频的 GUI 测试报告。也就是说,GUI 自动化测试框架会对测试执行整个过程进行屏幕录像并生成视频。
但是报告的体积通常都比较大,小的几 MB,大的上百 MB,这对测试报告的管理和实时传输非常不利。分析测试报告时,往往需要结合测试用例以及服务端的日志信息,视频报告这一点上也有所欠缺。
理想中的 GUI 测试报告应该是由一系列按时间顺序排列的屏幕截图组成,并且这些截图上可以高亮显示所操作的元素,同时按照执行顺序配有相关操作步骤的详细描述。

基于图片

利用 Selenium WebDriver 的 screenshot 函数在一些特定的时机(比如,页面发生跳转时,在页面上操作某个控件时,或者是测试失败时,等等)完成界面截图功能。
具体到代码实现,通常有两种方式:
  1. 使用 JavaScript 原本的操作函数进行组件颜色渲染;
  2. 在相关的 Hook 操作中调用 screenshot 函数;
由于这个 GUI 测试报告是基于 Web 展现的,所以我们可以在测试报告中直接提供递交缺陷的按钮,一旦发现问题直接递交缺陷,同时还可以把相关截图一起直接递交到缺陷管理系统,这将更大程度地提高整体效率。
现今的缺陷管理系统往往都有对外暴露 API 接口,我们完全可以利用这些 API 接口来实现自己的缺陷递交逻辑。

移动测试

一般移动测试可以分为 Web App、Native App 和 Hybrid App 这三类。
如果 Web 页面是基于自适应网页设计(即符合 Responsive Web 设计的规范),而且你的测试框架如果支持 Responsive Page,那么原则上你之前开发的运行在 PC Web 端的 GUI 自动化测试用例,不做任何修改就可以直接在移动端的浏览器上直接执行,当然运行的前提是你的移动端浏览器必须支持 Web Driver。
对 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_+ 被测进程名称”。

移动应用专项测试的思路和方法

  1. 交叉事件测试也叫中断测试,是指 App 执行过程中,有其他事件或者应用中断当前应用执行的测试。此类测试目前基本还都是采用手工测试的方式,并且都是在真机上进行,不会使用模拟器。
  2. 兼容性测试顾名思义就是,要确保 App 在各种终端设备、各种操作系统版本、各种屏幕分辨率、各种网络环境下,功能的正确性。兼容性测试,通常都需要在各种真机上执行相同或者类似的测试用例,所以往往采用自动化测试的手段。 同时,由于需要覆盖大量的真实设备,除了大公司会基于 Appium + Selenium Grid + OpenSTF 去搭建自己的移动设备私有云平台外,其他公司一般都会使用第三方的移动设备云测平台完成兼容性测试。
  3. 流量测试。由于 App 经常需要在移动互联网环境下运行,而移动互联网通常按照实际使用流量计费,所以如果你的 App 耗费的流量过多,那么一定不会很受欢迎。流量测试,往往借助于 Android 和 iOS 自带的工具进行流量统计,也可以利用 tcpdump、Wireshark 和 Fiddler 等网络分析工具。
  4. 耗电量测试也是反应一个移动应用能否可靠的关键方法之一。Android 通过 adb 命令“adb shell dumpsys battery”来获取应用的耗电量信息;iOS 通过 Apple 的官方工具 Sysdiagnose 来收集耗电量信息,然后,可以进一步通过 Instrument 工具链中的 Energy Diagnostics 进行耗电量分析。
  5. 弱网络测试,移动应用的测试需要保证在复杂网络环境下的质量。具体的做法就是:在测试阶段,模拟这些网络环境,在 App 发布前尽可能多地发现并修复问题。
  6. 边界测试是指,移动 App 在一些临界状态下的行为功能的验证测试,基本思路是需要找出各种潜在的临界场景,并对每一类临界场景做验证和测试。例如系统内存占用大于 90% 的场景;系统存储占用大于 95% 的场景;飞行模式来回切换的场景;App 不具有某些系统访问权限的场景,比如 App 由于隐私设置不能访问相册或者通讯录等;长时间使用 App,系统资源是否有异常,比如内存泄漏、过多的链接数等;出现 ANR 的场景;操作系统时间早于或者晚于标准时间的场景;时区切换的场景。

Appium 的实现原理

要真正理解 Appium 的内部原理,你可以把 Appium 分成三大部分,分别是 Appium Client、Appium Server 和设备端。
本质上,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 平台上的原生测试框架完成测试。

API 自动化测试

API 测试的基本步骤,主要分三步:
  1. 准备测试数据(这是可选步骤,不一定所有 API 测试都需要这一步);
  2. 通过 API 测试工具,发起对被测 API 的 request;
  3. 验证返回结果的 response。
对 API 的测试往往是使用 API 测试工具,比如常见的命令行工具 cURL、图形界面工具 Postman 或者 SoapUI、API 性能测试的 JMeter 等。

API 自动化测试框架

Postman

早期的 API 测试,往往都是通过类似 Postman 的工具完成的。但是,由于这类工具都是基于界面操作的,所以有以下两个问题亟待解决:
其一,当需要频繁执行大量的测试用例时,基于界面的 API 测试就显得有些笨拙。其二,基于界面操作的测试难以与 CI/CD 流水线集成。

Newman

集成 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 测试的场景,工作量相比 Postman 要大得多;对于单个 API 测试的场景,无法直接重用 Postman 里面已经积累的 Collection。
在实际工程中,这两个问题非常重要,而且必须要解决。因为公司管理层肯定无法接受相同工作的工作量直线上升,同时原本已经完成的部分无法继续使用,所以自动化生成 API 测试代码的技术也就应运而生了。

自动生成 API 测试代码

自动生成 API 测试代码是指,基于 Postman 的 Collection 生成基于代码的 API 测试用例。实际上则是通过字符串修改对 Postman Collection 转化成测试代码。其本质就是解析 Collection JSON 文件的各个部分,然后根据自研 API 框架的代码模板实现变量替换。具体可以分成三步:
  • 根据自研 API 框架的代码结构建立一个带有变量占位符的模板文件;
  • 通过 JSON 解析程序,按照 Collection JSON 文件的格式定义去提取 header、method 等信息;
  • 用提取得到的具体值替换之前模板文件中的变量占位符,这样就得到了可执行的自研框架的 API 测试用例代码;
开发了大量的基于代码的 API 测试用例后,会发现一个让人很纠结的问题:到底应该验证 API 返回结果中的哪些字段?
因为不可能对返回结果中的每一个字段都写 assert,通常情况下,只会针对关注的几个字段写 assert,而那些没写 assert 的字段也就无法被关注了。但对 API 测试来说,有一个很重要的概念是后向兼容性(backward compatibility)。API 的后向兼容性是指,发布的新 API 版本应该能够兼容老版本的 API。
后向兼容性除了要求 API 的调用参数不能发生变化外,还要求不能删减或者修改返回的 response 中的字段。因为这些返回的 response 会被下游的代码使用,如果字段被删减、改名或者字段值发生了非预期的变化,那么下游的代码就可能因为无法找到原本的字段,或者因为字段值的变化而发生问题,从而破坏 API 的后向兼容性。
在这样的背景下,诞生了“Response 结果变化时的自动识别”技术。也就是说,即使我们没有针对每个 response 字段都去写 assert,我们仍然可以识别出哪些 response 字段发生了变化。
具体实现的思路是,在 API 测试框架里引入一个内建数据库,推荐采用非关系型数据库(比如 MongoDB),然后用这个数据库记录每次调用的 request 和 response 的组合,当下次发送相同 request 时,API 测试框架就会自动和上次的 response 做差异检测,对于有变化的字段给出告警。而对于一些动态参数,解决办法是通过规则配置设立一个“白名单列表”,把那些动态值的字段排除在外。

微服务下的 API 自动化测试

由于微服务架构下,一个应用是由很多相互独立的微服务组成,每个微服务都会对外暴露接口,同时这些微服务之间存在级联调用关系,也就是说一个微服务通常还会去调用其他微服务,鉴于以上特点,微服务架构下的测试挑战主要来自于以下两个方面:过于庞大的测试用例数量;微服务之间的耦合关系。
采用微服务架构时,原本的单体应用会被拆分成多个独立模块,也就是很多个独立的 service,原本单体应用的全局功能将会由这些拆分得到的 API 共同协作完成。这时候一种既能保证 API 质量,又能减少测试用例数量的策略,基于消费者契约的 API 测试应运而生。
而对于微服务之间的耦合关系解耦的方式通常就是实现 Mock Service 来代替被依赖的真实 Service。实现这个 Mock Service 的关键点就是要能够模拟真实 Service 的 Request 和 Response。

基于消费者契约的 API 测试

按照传统的 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 的契约。
在实际项目中,我们不可能在每个 Service 前去放置这样一个代理。但是,微服务架构中往往会存在一个叫作 API Gateway 的组件,用于记录所有 API 之间相互调用关系的日志,我们可以通过解析 API Gateway 的日志分析得到每个 Service 的契约。

微服务测试的依赖解耦和 Mock Service

实现 Mock Service 的关键,就是要能够模拟被替代 Service 的 Request 和 Response。
消费者契约的本质就是 Request 和 Response 的组合,具体的表现形式往往是 JSON 文件,此时我们就可以用该契约的 JSON 文件作为 Mock Service 的依据,也就是在收到什么 Request 的时候应该回复什么 Response。
基于消费者契约的测试方法,由于收集到了完整的契约,所以基于契约的 Mock Service 完美地解决了 API 之间相互依赖耦合的问题。

代码测试

代码级测试的测试方法一定是一套测试方法的集合,而不是一个测试方法。 因为单靠一种测试方法不可能发现所有潜在的错误,一定是一种方法解决一部分或者一类问题,然后综合运用多种方法解决全部问题。
代码错误,可以分为“有特征”的错误和“无特征”的错误两大类。“有特征”的错误,可进一步分为语法特征错误、边界行为错误和经验特征错误;“无特征”的错误,主要包括算法错误和部分算法错误。

基本方法

代码级测试方法主要分为两大类,分别是静态方法和动态方法。静态方法,顾名思义就是在不实际执行代码的基础上发现代码缺陷的方法,又可以进一步细分为人工静态方法和自动静态方法;动态方法是指,通过实际执行代码发现代码中潜在缺陷的方法,同样可以进一步细分为人工动态方法和自动动态方法。

静态测试方法

人工静态方法

人工静态方法是指,通过人工阅读代码查找代码中潜在错误的方法,通常采用的手段包括,开发人员代码走查、结对编程、同行评审等。
使用最普遍的是同行评审。因为同行评审既能较好地保证代码质量,又不需要过多的人工成本投入,而且递交的代码出现问题后责任明确,另外代码的可追溯性也很好。结对编程的实际效果虽然不错,但是对人员的利用率比较低,通常被用于一些非常关键和底层算法的代码实现。
理论上,人工静态方法可以发现上述五类代码错误,但实际效果却并不理想。

自动静态方法

自动静态方法是指,在不运行代码的方式下,通过词法分析、语法分析、控制流分析等技术,并结合各种预定义和自定义的代码规则,对程序代码进行静态扫描发现语法错误、潜在语义错误,以及部分动态错误的一种代码分析技术。
自动静态方法可以发现语法特征错误、边界行为特征错误和经验特征错误这三类“有特征”的错误。但对于算法错误和部分算法错误这两种“无特征”的错误却无能为力。根本原因在于,自动静态方法并不清楚代码的具体业务逻辑。
目前,自动静态方法无论是在传统软件企业,还是在互联网软件企业都已经被广泛采用,往往会结合企业或项目的编码规范一起使用,并与持续集成过程紧密绑定。目前有很多工具都可以支持多种语言,比如 Sonar、Coverity 等,你可以根据实际需求来选择。

动态测试方法

人工动态方法

在代码级测试中,人工动态方法是最主要的测试手段,可以真正检测代码的逻辑功能,其关注点是“什么样的输入,执行了什么代码,产生了什么样的输出”,所以最善于发现算法错误和部分算法错误。
人工动态方法的单元测试即用驱动代码去调用被测函数,并根据代码的功能逻辑选择必要的输入数据的组合,然后验证执行被测函数后得到的结果是否符合预期。
但是到了实际项目时,你会发现单元测试太复杂了,因为测试用例设计时需要考虑的“输入参数”已经完全超乎想象了。
  1. 被测试函数的输入参数
  2. 被测试函数内部需要读取的全局静态变量
  3. 被测试函数内部需要读取的类成员变量
  4. 函数内部调用子函数获得的数据
  5. 函数内部调用子函数改写的数据
  6. 嵌入式系统中,在中断调用中改写的数据
除了预期输入的复杂性,还有预期输出的复杂性:
  1. 被测函数的返回值
  2. 被测函数的输出参数
  3. 被测函数所改写的成员变量和全局变量
  4. 被测函数中进行的文件更新、数据库更新、消息队列更新等
假设被测函数中调用了其他的函数,那么这些被调用的其他函数就是被测函数的关联依赖代码。大型的软件项目通常是并行开发的,所以经常会出现被测函数关联依赖的代码未完成或者未测试的情况,也就是出现关联依赖的代码不可用的情况。那么,为了不影响被测函数的测试,往往会采用桩代码来模拟不可用的代码,并通过打桩补齐未定义部分。
一般来讲桩函数主要有两个作用,一个是隔离和补齐,另一个是实现被测函数的逻辑控制。用于实现隔离和补齐的桩函数实现比较简单,只需拷贝原函数的声明,加一个空的实现,可以通过编译链接就可以了。用于实现控制功能的桩函数是最常用的,实现起来也比较复杂,需要根据测试用例的需要,输出合适的数据作为被测函数的内部输入。
目前,不同的编程语言对应有不同的单元测试框架,比如,对 Java 语言最典型的是 Junit 和 TestNG,对于 C 语言比较常用的是 Google Test 等。

自动动态方法

自动动态方法,又称自动边界测试方法,指的是基于代码自动生成边界测试用例并执行,以捕捉潜在的异常、崩溃和超时的方法。
自动动态方法的重点是:如何实现边界测试用例的自动生成。解决这个问题最简单直接的方法是,根据被测函数的输入参数生成可能的边界值。
具体来讲,任何数据类型都有自己的典型值和边界值,我们可以预先为它们设定好典型值和边界值,然后组合就可以生成了。比如,函数 int func(int a, char *s),就可以按下面的三步来生成测试用例集。
  1. 定义各种数据类型的典型值和边界值。
  2. 根据被测函数的原形,生成测试用例代码模板。
  3. 将参数 @a@和 @s@的各种取值循环组合,分别替换模板中的相应内容,即可生成用例集。
自动动态方法,可以覆盖边界行为特征错误, 通常能够发现“忘记处理某些输入”引起的错误(因为容易忘记处理的输入,往往是“边界”输入)。但是它对于发现算法错误无能为力,毕竟工具不可能了解代码所要实现的功能逻辑。

性能测试

目前,对软件性能最普遍的理解就是软件处理的及时性。但其实,从不同的系统类型,以及不同的视角去讨论软件性能,都会有所区别。对于不同类型的系统,软件性能的关注点各不相同。

软件性能与性能指标

终端用户是软件系统的最终使用者,他们对软件性能的反馈直接决定了这个系统的应用前景。而软件开发人员、运维人员、性能测试人员,对性能测试的关注点则直接决定了一个系统交付到用户手中的性能。
  • 从终端用户的维度来讲,软件性能表现为用户进行业务操作时的主观 响应时间。具体来讲就是,从用户在界面上完成一个操作开始,到系统把本次操作的结果以用户能察觉的方式展现出来的全部时间。对终端用户来说,这个时间越短体验越好。
  • 从软件系统运维的角度,软件性能除了包括单个用户的响应时间外,更 要关注大量用户并发访问时的负载,以及可能的更大负载情况下的系统健康状态、并发处理能 力、当前部署的系统容量、可能的系统瓶颈、系统配置层面的调优、数据库的调优,以及长时间 运行稳定性和可扩展性。
  • 从软件系统开发的角度来讲,软件性能关注的是性能相关的设计和实现细节,这几乎涵盖了软件设计和开发的全过程。在软件设计开发人员眼中,软件性能通常会包含算法设计、架构设计、性能最佳实践、数据库相 关、软件性能的可测试性这五大方面。其中,每个方面关注的点,也包括很多。
  • 从性能工程的角度看,性能测试工程师关注的是算法设计、架构设计、性能最佳实践、数据库相关、软件性能的可测试性这五大方面。在系统架构师、DBA,以及开发人员的协助下,性能测试人员既要能够准确把握软件的性能需求,又要能够准确定位引起“不好”性能表现的制约因素和根源,并提出相应的解决方案。
而衡量软件性能的三个最常用的指标是并发用户数、响应时间,以及系统吞 吐量。
  • 并发用户数,是性能需求与测试最常用,也是最重要的指标之一。它包含了业务层面和后端服务 器层面的两层含义。
    1. 业务层面的并发用户数,指的是实际使用系统的用户总数。但是,单靠这个指标并不能反映系统实际承载的压力,我们还要结合用户行为模型才能得到系统实际承载的压力。
    2. 后端服务器层面的并发用户数,指的是“同时向服务器发送请求的数量”,直接反映了系统实际承载的压力。
  • 响应时间,反映了完成某个操作所需要的时间,其标准定义是“应用系统从请求发出开始,到客户端接收到最后一个字节数据所消耗的时间”,是用户视角软件性能的主要体现。响应时间,分为前端展现时间和系统响应时间两部分。
    1. 前端时间,又称呈现时间,取决于 客户端收到服务器返回的数据后渲染页面所消耗的时间。
    2. 系统响应时间,又可以进一步划分为 Web 服务器时间、应用服务器时间、数据库时间,以及各服务器间通信的网络时间。
  • 系统吞吐量,是最能直接体现软件系统负载承受能力的指标。以不同方式表达的吞吐量可以说明不同层次的问题。
    1. “Bytes/Second”和“Pages/Second”表示的吞吐量,主要受网络设置、服务器架构、应 用服务器制约。
    2. “Requests/Second”表示的吞吐量,主要受应用服务器和应用本身实现的制约。

基本方法与应用领域

根据在实际项目中的实践经验,常用的性能测试方法分为七大类:后端性能测试(Backend Performance Test)、前端性能测试(Front-end Performance Test)、代码级性能测试 (Code-level Performance Test)、压力测试(Load/Stress Test)、配置测试 (Configuration Test)、并发测试(Concurrence Test),以及可靠性测试(Reliability Test)。
  1. 后端性能测试,是通过性能测试工具模拟大量的并发用户请求,然后获取系统性能的各项指标, 并且验证各项指标是否符合预期的性能需求的测试手段。平时听到的性能测试,大多数情况下指的是后端性能测试,也就是服务器端性能测试。
  2. 前端性能测试并没有一个严格的定义和标准。前端性能关注的是浏览器端的页面渲染时间、资源加载顺序、请求数量、前端缓存使用情况、资源压缩等内容,希望借此找到页面加载过程中比较耗时的操作和资源,然后进行有针对性的优化,终达到优化终端用户在浏览器端使用体验的目的。业界普遍采用的前端测试方法,是雅虎(Yahoo)前端团队总结的 7 大类 35 条前端优化规则。
  3. 代码级性能测试是指在单元测试阶段就对代码的时间性能和空间性能进行必要的测试和评估,以防止底层代码的效率问题在项目后期才被发现的尴尬。常使用的改造方法是将原本只会执行一次的单元测试用例连续执行 n 次,这个 n 的取值范围通常是 2000~5000。统计执行 n 次的平均时间。如果这个平均时间比较长(也就是单次函数调用时间比较长) 的话,比如已经达到了秒级,那么通常情况下这个被测函数的实现逻辑一定需要优化。
  4. 压力测试,通常指的是后端压力测试,一般采用后端性能测试的方法,不断对系统施加压力,并验证系统化处于或长期处于临界饱和阶段的稳定性以及性能指标,并试图找到系统处于临界状态 时的主要瓶颈点。所以,压力测试往往被用于系统容量规划的测试。
  5. 配置测试主要用于观察系统在不同配置下的性能表现,通常使用后端性能测试的方法。通过性能基准测试(Performance Benchmark)建立性能基线(Performance Baseline)在此基础上,调整配置。基于同样的性能基准测试,观察不同配置条件下系统性能的差异,根本目的是要找到特定压力模式下的佳配置。
  6. 并发测试,指的是在同一时间,同时调用后端服务,期间观察被调用服务在并发情况下的行为表现,旨在发现诸如资源竞争、资源死锁之类的问题。“集合点并发” 是并发测试的常见手段,为了达到准确控制后端服务并发数的目的,我们需要让某些并发用户到达该集合点时,先处于等待状态,直到参与该集合的全部并发用户都到达时,再一起向后端服务发起请求。简单地说,就是先到的并发用户要等着,等所有并发用户都到了以后,再集中向后端服务发起请求。并且在实际项目中,测试并发数需要稍微大于预期并发数。
  7. 可靠性测试是验证系统在常规负载模式下长期运行的稳定性。其本质就是通过长时间模拟真实的系统负载来发现系 统潜在的内存泄漏、链接池回收等问题。
不同的性能测试方法适用于不同的应用领域去解决不同的问题,这里“不同的应用领域”主要包 括能力验证、能力规划、性能调优、缺陷发现这四大方面。每个应用领域可以根据自身特点,选 择合适的测试方法。
  1. 能力验证是常用,也是容易理解的性能测试的应用领域,主要是验证“某系统能否在 A 条件下具有 B 能力”,通常要求在明确的软硬件环境下,根据明确的系统性能需求设计测试方案和用例。能力验证这个领域常使用的测试方法,包括后端性能测试、压力测试和可靠性测试。
  2. 能力规划关注的是,如何才能使系统达到要求的性能和容量。通常情况下,我们会采用探索性测 试的方式来了解系统的能力。能力规划常使用的测试方法,主要有后端性能测试、压力测试、配置测试和可靠性测试。
  3. 性能调优,其实是性能测试的延伸。在一些大型软件公司,会有专门的性能工程 (Performance Engineering)团队,除了负责性能测试的工作外,还会负责性能调优。性能调优主要解决性能测试过程中发现的性能瓶颈的问题,通常会涉及多个层面的调整,包括硬件设备选型、操作系统配置、应用系统配置、数据库配置和应用代码实现的优化等等。
  4. 缺陷发现,是一个比较直接的应用领域,通过性能测试的各种方法来发现诸如内存泄露、资源竞争、不合理的线程锁和死锁等问题。缺陷发现,常用的测试方法主要有并发测试、压力测试、后端性能测试和代码级性能测试。

后端性能测试

完整的后端性能测试应该包括性能需求获取、性能场景设计、性能测试脚本开发、性能场景实现、性能测试执行、性能结果报告分析、性能优化和再验证。在这其中,后端性能测试工具主要在性能测试脚本开发、性能场景实现、性能测试执行这三个步骤中发挥作用,而其他环节都要依靠性能测试工程师的专业知识完成。
后端性能测试工具会基于客户端与服务器端的通信协议,构建模拟业务操作的虚拟用户脚 本。对于目前主流的 Web 应用,通常是基于 HTTP/HTTPS 协议;对于 Web Service 应用,是 基于 Web Service 协议;至于具体基于哪种协议,需要和开发人员或者架构师确认。
后端性能测试工具会以多线程或多进程的方式并发执行虚 拟用户脚本,来模拟大量并发用户的同时访问,从而对服务器施加测试负载。其中,实际发起测试负载的机器称为压力产生器。受限于 CPU、内存,以及网络带宽等硬件资源,一台压力产生器能够承载的虚拟用户数量是有限的,当需要发起的并发用户数量超过 了单台压力产生器能够提供的极限时,就需要引入多台压力产生器合作发起需要的测试负载。
在施加测试负载的整个过程中,后端性能测试工具除了需要监控和收集被测系统的各种性能数据以外,还需要监控被测系统各个服务器的各种软硬件资源。比如,后端性能测试工具需要 监控应用服务器、数据库服务器、消息队列服务器、缓存服务器等各种资源的占用率。通常把完成监控和数据收集的模块称为系统监控器。
最后,测试执行完成后,后端性能测试工具会将系统监控器收集的所有信息汇总为完整测试报告,后端性能测试工具通常能够基于该报告生成各类指标的各种图表,还能将多个指标关联在一起进行综合分析来找出各个指标之间的关联性。完成这部分工作的模块称为测试结果分析器。
性能测试场景设计,是后端性能测试中的重要概念,也是压力控制器发起测试负载的依据。目的是要描述性能测试过程中所有与测试负载以及监控相关的内容。通常来讲,性能测试场景设计主要会涉及以下部分:
  1. 测试负载组成:虚拟用户脚本,各个虚拟用户脚本的并发数量,总的用户并发数量。
  2. 负载策略:加压策略,减压策略,最大负载运行时间,延时策略。
  3. 资源监控范围定义:操作系统级别,应用服务器级别,数据库服务器级别,缓存集群级别。
  4. 终止方式:脚本出错时的处理,负载熔断机制。
  5. 负载产生规划:压力产生器数量,网络带宽请求。

前端性能测试

这里以 WebPagetest 为例,介绍前端页面测试的几个重要指标。
  1. First Byte Time,指的是用户发起页面请求到接收到服务器返回的第一个字节所花费的时间。这个指标反映了后端服务器处理请求、构建页面,并且通过网络返回所花费的时间。
  2. Keep-alive Enabled 就是,要求每次请求使用已经建立好的链接。它属于服务器上的配置,不需要对页面本身进行任何更改,启用了 Keep-alive 通常可以将加载页面的时间减少 40%~50%,页面的请求数越多,能够节省的时间就越多。
  3. Compress Transfer 是在传输过程中将 HTTP 报文进行压缩。如果将页面上的各种文本类的资源,比如 Html、JavaScript、CSS 等,进行压缩传输,将会减少网络传输的数据量,同时由于 JavaScript 和 CSS 都是页面上最先被加载的部分,所以减小这部分的数据量会加快页面的加载速度,同时也能缩短 First Byte Time。
  4. Compress Images 是指将页面中的图片进行压缩传输。普通 JPEG 文件存储方式是按从上到下的扫描方式,把每一行顺序地保存在 JPEG 文件中。打开这个文件显示它的内容时,数据将按照存储时的顺序从上到下一行一行地被显示,直到所有的数据都被读完,就完成了整张图片的显示。渐进式 JPEG 包含多次扫描,然后将扫描顺序存储在 JPEG 文件中。打开文件的过程,会先显示整个图片的模糊轮廓,随着扫描次数的增加,图片会变得越来越清晰。这种格式的主要优点是在网络较慢时,通过图片轮廓就可以知道正在加载的图片大概是什么。
  5. Cache Static Content 指的是动态页面的静态化缓存。一般情况下,页面上的静态资源不会经常变化,所以如果你的浏览器可以缓存这些资源,那么当重复访问这些页面时,就可以从缓存中直接使用已有的副本,而不需要每次向 Web 服务器请求资源。这种做法,可以显著提高重复访问页面的性能,并减少 Web 服务器的负载。
  6. Effective use of CDN 指的是采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区的网络供应商机房内,当用户访问网站时,利用全局负载技术将用户的访 问指向距离最近的、工作正常的缓存服务器上,由缓存服务器直接响应用户请求。
  7. Start Render,指的是浏览器开始渲染的时间,从用户角度看就是在页面上看到第一个内容的时间。理论上讲,Start Render 时间主要由三部分组成,分别是“发起请求到服务器返回第一个字节的时间(也就是 First Byte 时间)”,“从服务器加载 HTML 文档的时间”,以及“HTML 文档头部解析完成所需要的时间”,因此影响 Start Render 时间的因素就包括服务器响应时间、网络传输时间、HTML 文档的大小以及 HTML 头中的资源使用情况。
  8. First Interactive,可以简单地理解为最早的页面可交互时间。页面中可交互的内容,包括很多种类,比如点击一个链接、点击一个按钮都属于页面可交互的范畴。First Interactive 时间的长短对用户体验的影响十分重要,决定着用户对页面功能的使用,这个值越短越好。
  9. Speed Index 用微积分的方法来计算并衡量用户打开页面时的体验并和其他类似页面时的情况作对比,通常来讲,它的值越小越好。
用 WebPagetest 执行前端测试时,所有的操作都是基于界面操作的,不利于与 CI/CD 的流水线集成。要解决这个问题,就必须引入 WebPagetest API Wrapper。WebPagetest API Wrapper 是一款基于 Node.js,调用了 WebPagetest 提供的 API 的命令行工具。也就是说,你可以利用这个命令行工具发起基于 WebPagetest 的前端性能测试,这样就可以很方便地与 CI/CD 流水线集成了。

测试数据

测试数据的产生主要有 On the Fly 和 Out of Box 两种方法。测试数据准备方法主要可以分为四类:
  1. 基于 GUI 操作生成测试数据;
  2. 通过 API 调用生成测试数据;
  3. 通过数据库操作生成测试数据;
  4. 综合运用 API 和数据库的方式生成测试数据。

数据准备方法对比

基于 GUI 操作生成测试数据,是最原始的创建测试数据的方法。这个方法的优点是简单直接,在技术上没有任何复杂性,而且所创建的数据完全来自于真实的业务流程,可以最大程度保证数据的正确性。但是,该方法的缺点也十分明显,主要体现在以下这四个方面:
  • 创建测试数据的效率非常低。一是因为每次执行 GUI 业务操作都只能创建一条数据,二是因为基于 GUI 操作的执行过程比较耗时。
  • 基于 GUI 的测试数据创建方法不适合封装成测试数据工具。
  • 测试数据成功创建的概率不会太高。测试数据准备的成功率受限于 GUI 自动化执行的稳定性,而且任何界面的变更都有可能引发测试数据创建的失败。
  • 会引入不必要的测试依赖。比如,你的被测对象是用户登录功能,通过 GUI 页面操作准备这个已经注册的用户,就首先要保证用户注册功能没有问题,而这显然是不合理的。
通过 API 调用生成测试数据,是目前主流的测试数据生成方法。其实,当我们通过操作 GUI 界面生成测试数据时,实际的业务操作往往是由后端的 API 调用完成的。但是,该方法也不是完美无瑕的,其缺点主要表现在:
  • 并不是所有的测试数据创建都有对应的 API 支持。也就是说,并不是所有的数据都可以通过 API 调用的方式创建,有些操作还是必须依赖于数据库的 CRUD 操作。
  • 创建一条业务线上的测试数据,往往需要按一定的顺序依次调用多个 API,并且会在多个 API 调用之间传递数据,这也无形中增加了测试数据准备函数的复杂性。
  • 对于需要批量创建海量数据的场景,还是会力不从心。
因此,业界往往还会通过数据库的 CRUD 操作生成测试数据。常见的做法是,将创建数据需要用到的 SQL 语句封装成一个个的测试数据准备函数,当我们需要创建数据时,直接调用这些封装好的函数即可。当然,这个方法的缺点也非常明显,主要体现在以下几个方面:
  • 一个前端操作引发的数据创建,往往会修改很多张表,因此封装的数据准备函数的维 护成本要高得多。
  • 容易出现数据不完整的情况,比如一个业务操作,实际上在一张主表和一张附表中插入了记录,但是基于数据库操作的数据创建可能只在主表中插入了记录,这种错误一般都会比较隐蔽,往往 只在一些特定的操作下才会发生异常。
  • 当业务逻辑发生变化时,即 SQL 语句有变化时,需要维护和更新已经封装的数据准备函数。
综上所述,每种测试数据都具有其缺陷性,需要灵活结合这三种测试方法,才能保证测试的高效和稳定。

软件测试风格

探索式测试

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)的风格看起来类似,都是依靠测试工程师的经 验和直觉来即兴发挥,快速地试验被测试应用,并不停地调整测试策略。但是,探索式测试相比即兴测试更强调及时“反馈”的重要性。
在探索式测试中,测试工程师不断提出假设,通过测试执行去检验假设,通过解读测试结果证实或推翻假设。在这个迭代过程中,测试工程师不断完善头脑中被测试应用的知识体系,并建立被测应用的模型,然后利用模型、过往经验,以及测试技术驱动进一步的测试。

测试驱动开发

TDD 并不是一门技术,而是一种开发理念。它的核心思想,是在开发人员实现功能代码前,先设计好测试用例的代码,然后再根据测试用例的代码编写产品的功能代码,最终目的是让开发前设计的测试用例代码都能够顺利执行通过。站在全局的角度来看,TDD 的整个过程遵循以下流程:
  1. 为需要实现的新功能添加一批测试
  2. 运行所有测试,看看新添加的测试是否失败
  3. 编写实现软件新功能的实现代码
  4. 再次运行所有的测试,看是否有测试失败
  5. 重构代码
  6. 重复以上步骤直到所有测试通过
首先,需要控制 TDD 测试用例的粒度。应该先把测试用例分解成更小粒度的任务列表,保证每一个任务列表都是一个最小的功能模块。在开发过程中,要把测试用例当成用户,不断分析他可能会怎样调用这个功能,大到功能的设计 是用类还是接口,小到方法的参数类型,都要充分考虑到用户的使用场景。
其次,要注意代码的简洁和高效,通过重构保证最终交付代码的优雅和简洁。。所有功能代码都完成,所有测试都通过之后,就要考虑重构了。这里可以考虑类名、方法名甚至变量名命名,是否规范且有意义,太长的类可以考虑拆分;从系统角度检查是否有重复代码,是否有可以合并的代码。

精准测试

精准测试可以参考《星云精准测试白皮书》类测试平台,其主要针对解决传统测试方法的5大短板:
  1. 测试的维护成本日益升高
  2. 测试过程的低效
  3. 缺乏有效的回归用例选取机制
  4. 测试结果的可信度不高
  5. 无论是白盒测试技术还是黑盒测试技术都有其局限性
针对这五个短板,精准测试应运而生。精准测试的核心思想,可以概括为以下5点:
  1. 精准测试是对传统测试的补充。精准测试是基于传统测试数据的,并不会改变传统的软件测试方法,更不会取代传统测试。也就是说,精准测试在不改变原有测试集的基础下,能够优化测试过程和数据,提高测试效率。
  2. 精准测试采用的是黑盒测试与白盒测试相结合的模式。在执行黑盒测试时,收集程序自动产生的白盒级别的运行数据,然后通过可视化或者智能算法识别出测试未覆盖的点,继而引导开发人员和测试人员有的放矢地补充测试用例。
  3. 精准测试的数据可信度高。精准测试的数据都是由系统自动录入和管理的,人工无法直接修改数据,因此我们可以直接将传统测试产生的数据导入精准测试系统,用于测试结果的分析,从而使测试结果具有更高的可信度。
  4. 精准测试过程中,不直接面对产品代码。精准测试通过算法和软件实现对测试数据和过程的采集,因此并不会直接面向代码,也就不会强依赖于产品代码。
  5. 精准测试是与平台无关的、多维度的测试分析算法系统。精准测试系统是一种通用的测试分析系统,独立于任何测试平台,其内部算法和业务无关,因此适用于各种不同的产品。

渗透测试

渗透测试指的是,由专业安全人员模拟骇客,从其可能存在的位置对系统进行攻击测试,在真正的骇客入侵前找到隐藏的安全漏洞,从而达到保护系统安全的目的。渗透测试的五种常用测试方法有:
  1. 有针对性的测试,属于研发层面的渗透测试。参与这类测试的人员,可以得到被测系统的内部资料,包括部署信息、网络信息、详细架构设计,甚至是产品代码。由公司内部员工和专业渗透测试团队共同完成的。其中,公司内部员工不仅 要负责提供安全测试所需要的基础信息,同时也要负责业务层面的安全测试;而专业渗透测试团队,则更多关注业务以外的、更普适的安全测试。
  2. 外部测试,是针对外部可见的服务器和设备(包括:域名服务器(DNS)、Web 服务器或防火墙、电子邮箱服务器等等),模拟外部攻击者对其进行攻击,检查它们是否能够被入侵,以及如果被成功入侵了,会被入侵到系统的哪一部分、又会泄露多少资料。一般情况下,外部测试是由内部的测试人员或者专业渗透测试团队,在假定完全不清楚系统内部情况的前提下开展的。
  3. 内部测试,是由测试工程师模拟内部人员,在内网(防火墙以内)进行攻击,因此测试人员会拥有较高的系统权限,也能够查看各种内部资料,目的是检查内部攻击可以给系统造成什么程度的损害。
  4. 盲测,指的是在严格限制提供给测试执行人员或团队信息的前提下,由他们来模拟真实攻击者的行为和上下文。通常,测试人员可能只被告知被测系统公开的信息,而对系统细节以及内部实现一无所知。
  5. 双盲测试,不光测试人员对系统内部知之甚少,而且被测系统内部也只有极少数人知道正在进行安全测试。因此,双盲测试可以反映软件系统最真实的安全状态,能够有效地检测系统在正常情况下,对安全事件的监控和处理能力是否合格。
除此之外,再来说说渗透测试的 5 个主要步骤:
  1. 规划和侦察
  2. 安全扫描
  3. 获取访问权限
  4. 维持访问权限
  5. 入侵分析。
通过以上五步,一篇渗透测试报告就可以出炉了。报告内容包括特定漏洞名称,执行具体步骤,能够被访问的数据,入侵持续时间。

基于模型的测试

Model-Based-Testing,简称 MBT。是自动化测试的一个分支。它是将测试用例的设计依托于被测系统的模型,并基于该模型自动生成测试用例的技术。其中,这个被测系统的模型表示了被测系统行为的预期,也可以说是代表了我们对被测系统的预期。
所以,执行 MBT 的过程就好比你把软件系统的设计画为了一张由节点和边构成的数据结构意义上的“图”,然后通过一定的算法(比如,深度遍历或者广度遍历)来尽可能完整覆盖图中全部 的可能路径的过程。
根据被测系统本身的特点,我们常用的模型主要有限状态机、状态图,以及 UML 三种。其中,有限状态机和状态图比较适合于用状态或者事件驱动的系统,而 UML 比较适合于靠业务流程驱动的系统。
  1. 有限状态机可以帮助测试人员根据选中的输入来评估输出,不同的输入组合对应着不同的系统状态。
  2. 状态图是有限状态机的延伸,用于描述系统的各种行为,尤其适用于复杂且实时的系统。
  3. UML 即统一建模语言,是一种标准化的通用建模语言。UML 有自己定义的图形库,里面包含了丰富的图形用以描述系统、流程等。UML 可以通过创建可视化模型,来描述非常复杂的系统行为。
这些模型测试,都有一些对应的工具:
  1. BPM-X,其根据不同的标准(比如,语句、分支、路径、条件)从业务流程模型创建测试用例。它还可以从多个建模工具导入模型,并可以将测试用例导出到 Excel、HP Quality Center 等。 这个工具,适用于业务流程比较清晰直观的场景。
  2. fMBT 是一组免费的、用于全自动测试生成和执行的工具,也是一组支持高水平测试自动化的实用程序和库,主要被应用在 GUI 测试中。fMBT 包括用于多平台 GUI 测试的 Python 库,用于编辑、调试、运行和记录 GUI 测试脚本的 工具,以及用于编辑和可视化分析测试模型和生成的测试工具。
  3. GraphWalker 以有向图的形式读取模型,主要支持 FSM、EFSM 模型。它读取这些模型,然后生成测试路径。GraphWalker 除了适用于 GUI 测试外,更适合于多状态以及基于事件驱动的状 态转换的后台系统。另外,GraphWalker 还支持从有限状态机中生成测试用例。
除此之外,市面上还有很多 MBT 测试工具,比如 GSL、JSXM、MaTeLo、MBT Suite 等。可以自行百度了解它们的特点和适用场景,从而选取合适自己的工具。

评论