开篇词 | 从“小工”到“专家”,我的软件测试修炼之道
随着自动化测试用例设计与开发、测试框架选型、测试框架自行研发、测试基础架构设计以及最新的测试服务化(Test as a Service,TaaS)等一系列技术的变革与发展,测试项目几乎涵盖了所有种类,包括嵌入式系统测试、金融平台单元测试、平台 SDK 测试、轨道交通安全软件测试、Web Service 测试、大型电商网站 GUI 自动化以及性能全链路压测等。
第一步,成为互联网时代合格的测试工程师。
你必须具有快速学习的能力,能迅速掌握被测软件的业务功能与内部架构,并在此基础上运用各种测试方法,尽可能多地发现潜在缺陷,并能够在已知缺陷的基础上进一步发现相关的连带缺陷。
从知识体系上看,你需要有比开发人员更全面的计算机基础知识,还需要了解互联网的基础架构、安全攻击、软件性能、用户体验和常见缺陷等知识。
从测试技术上看,你需要能够使用常见的测试框架或者工具,需要具有一定的自动化测试脚本的开发能力,这可以把你从大量重复的工作中解放出来,然后你才能有时间去做更有意思的工作。
第二步,成为互联网时代优秀的测试工程师。
首先,合格的测试工程师关注的是纯粹的测试,而优秀的测试工程师关注更多的是软件整体的质量,需要根据业务风险以及影响来制定测试策略,有效控制测试的时间和成本,并且能够对测试框架以及工具做出适合项目需求的选型。
第三步,成为互联网时代的测试架构师。
面对大量测试用例的执行,无论是 GUI 还是 API,都需要一套高效的能够支持高并发的测试执行基础架构;再比如,面对测试过程中的大量差异性数据要求,需要统一的测试数据准备平台;再比如,为了可以更方便地和持续集成与发布系统(CI/CD)以解耦的形式做集成,需要统一发起测试执行的接口。
这样的例子还有很多,如果你已经能够站在这样的高度看待软件测试,那么恭喜你,你已经具备了测试架构师的视野。当然,你还必须对一些前沿的测试方法和技术有自己的理解,并能够在恰当的时候、因地制宜地把它们应用到实际项目中。
01 | 你真的懂测试吗?从“用户登录”测试谈起
功能测试用例:
基本
输入已注册的用户名和正确的密码,验证是否登录成功;
输入已注册的用户名和不正确的密码,验证是否登录失败,并且提示信息正确;
输入未注册的用户名和任意密码,验证是否登录失败,并且提示信息正确;
用户名和密码两者都为空,验证是否登录失败,并且提示信息正确;
用户名和密码两者之一为空,验证是否登录失败,并且提示信息正确;
如果登录功能启用了验证码功能,在用户名和密码正确的前提下,输入正确的验证码,验证是否登录成功;
如果登录功能启用了验证码功能,在用户名和密码正确的前提下,输入错误的验证码,验证是否登录失败,并且提示信息正确。
高级
- 用户名和密码是否大小写敏感;
- 页面上的密码框是否加密显示;
- 后台系统创建的用户第一次登录成功时,是否提示修改密码;
- 忘记用户名和忘记密码的功能是否可用;
- 前端页面是否根据设计要求限制用户名和密码长度;
- 如果登录功能需要验证码,点击验证码图片是否可以更换验证码,更换后的验证码是否可用;
- 刷新页面是否会刷新验证码;
- 如果验证码具有时效性,需要分别验证时效内和时效外验证码的有效性;
- 用户登录成功但是会话超时后,继续操作是否会重定向到用户登录界面;
- 不同级别的用户,比如管理员用户和普通用户,登录系统后的权限是否正确;
- 页面默认焦点是否定位在用户名的输入框中;
- 快捷键 Tab 和 Enter 等,是否可以正常使用。
一个质量过硬的软件系统,除了显式功能性需求以外,其他的非功能性需求即隐式功能性需求也是极其关键的。
安全性测试用例:
- 用户密码后台存储是否加密;
- 用户密码在网络传输过程中是否加密;
- 密码是否具有有效期,密码有效期到期后,是否提示需要修改密码;
- 不登录的情况下,在浏览器中直接输入登录后的 URL 地址,验证是否会重新定向到用户登录界面;
- 密码输入框是否不支持复制和粘贴;
- 密码输入框内输入的密码是否都可以在页面源码模式下被查看;
- 用户名和密码的输入框中分别输入典型的“SQL 注入攻击”字符串,验证系统的返回页面;
- 用户名和密码的输入框中分别输入典型的“XSS 跨站脚本攻击”字符串,验证系统行为是否被篡改;
- 连续多次登录失败情况下,系统是否会阻止后续的尝试以应对暴力破解;
- 同一用户在同一终端的多种浏览器上登录,验证登录功能的互斥性是否符合设计预期;
- 同一用户先后在多台终端的浏览器上登录,验证登录是否具有互斥性。
性能压力测试用例:
- 单用户登录的响应时间是否小于 3 秒;
- 单用户登录时,后台请求数量是否过多;
- 高并发场景下用户登录的响应时间是否小于 5 秒;
- 高并发场景下服务端的监控指标是否符合预期;
- 高集合点并发场景下,是否存在资源死锁和不合理的资源等待;
- 长时间大量用户连续登录和登出,服务器端是否存在内存泄漏。
兼容性测试用例:
- 不同浏览器下,验证登录页面的显示以及功能正确性;
- 相同浏览器的不同版本下,验证登录页面的显示以及功能正确性;
- 不同移动设备终端的不同浏览器下,验证登录页面的显示以及功能正确性;
- 不同分辨率的界面下,验证登录页面的显示以及功能正确性。
一个优秀的测试工程师必须具有很宽广的知识面,如果你不能对被测系统的设计有深入的理解、不明白安全攻击的基本原理、没有掌握性能测试的基本设计方法,很难设计出“有的放矢”的测试用例。
在绝大多数的软件工程实践中,测试由于受限于时间成本和经济成本,是不可能去穷尽所有可能的组合的,而是采用基于风险驱动的模式,有所侧重地选择测试范围和设计测试用例,以寻求缺陷风险和研发成本之间的平衡。
总结
首先,对于高质量的软件测试,用例设计不仅需要考虑明确的显式功能性需求,还要涉及兼容性、安全性和性能等一系列的非功能性需求,这些非功能性需求对软件系统的质量有着举足轻重的作用。
其次,优秀的测试工程师必须具有宽广的知识面,才能设计出有针对性、更易于发现问题的测试用例。
最后,软件测试的用例设计是不可穷尽的,工程实践中难免受制于时间成本和经济成本,所以优秀的测试工程师需要兼顾缺陷风险和研发成本之间的平衡。
02 | 如何设计一个“好的”测试用例?
“好的”测试用例一定是一个完备的集合,它能够覆盖所有等价类以及各种边界值,而跟能否发现缺陷无关。
“好的”测试用例必须具备哪些特征?
一个“好的”测试用例,必须具备以下三个特征。
- 整体完备性: “好的”测试用例一定是一个完备的整体,是有效测试用例组成的集合,能够完全覆盖测试需求。
- 等价类划分的准确性: 指的是对于每个等价类都能保证只要其中一个输入测试通过,其他输入也一定测试通过。
- 等价类集合的完备性: 需要保证所有可能的边界值和边界条件都已经正确识别。
做到了以上三点,就可以肯定测试是充分且完备的,即做到了完整的测试需求覆盖。
对大多数的软件测试而言,综合使用等价类划分、边界值分析和错误推测这三大类方法就足够了。
具体到测试用例本身的设计,有两个关键点需要你注意。
- 从软件功能需求出发,全面地、无遗漏地识别出测试需求是至关重要的,这将直接关系到用例的测试覆盖率。 比如,如果你没有识别出用户登录功能的安全性测试需求,那么后续设计的测试用例就完全不会涉及安全性,最终造成重要测试漏洞。
- 对于识别出的每个测试需求点,需要综合运用等价类划分、边界值分析和错误推测方法来全面地设计测试用例。 这里需要注意的是,要综合运用这三种方法,并针对每个测试需求点的具体情况,进行灵活选择。
以“用户登录”的功能性测试需求为例,你首先应该对“用户名”和“密码”这两个输入项分别进行等价类划分,列出对应的有效等价类和无效等价类,对于无效等价类的识别可以采用错误猜测法(比如,用户名包含特殊字符等),然后基于两者可能的组合,设计出第一批测试用例。
等价类划分完后,你需要补充“用户名”和“密码”这两个输入项的边界值的测试用例,比如用户名为空(NULL)、用户名长度刚刚大于允许长度等。
用例设计的其他经验
除了上面介绍的方法外,我再跟你分享三个独家“秘籍”,希望能够帮你设计出“好的”测试用例集。
- 只有深入理解被测试软件的架构,你才能设计出“有的放矢”的测试用例集,去发现系统边界以及系统集成上的潜在缺陷。
作为测试工程师,切忌不能把整个被测系统看作一个大黑盒,你必须对内部的架构有清楚的认识,比如数据库连接方式、数据库的读写分离、消息中间件 Kafka 的配置、缓存系统的层级分布、第三方系统的集成等等。 - 必须深入理解被测软件的设计与实现细节,深入理解软件内部的处理逻辑。
单单根据测试需求点设计的用例,只能覆盖“表面”的一层,往往会覆盖不到内部的处理流程、分支处理,而没有覆盖到的部分就很可能出现缺陷遗漏。在具体实践中,你可以通过代码覆盖率指标找出可能的测试遗漏点。
同时,切忌不要以开发代码的实现为依据设计测试用例。因为开发代码实现的错误会导致测试用例也出错,所以你应该根据原始需求设计测试用例。 - 需要引入需求覆盖率和代码覆盖率来衡量测试执行的完备性,并以此为依据来找出遗漏的测试点。 关于什么是需求覆盖率和代码覆盖率,我会在后续的文章中详细介绍。
总结
最后,我来简单总结一下今天的主要内容。
首先,你需要明白,“好的”测试用例一定是一个完备的集合,它能够覆盖所有等价类以及各种边界值,而能否发现软件缺陷并不是衡量测试用例好坏的标准。
其次,设计测试用例的方法有很多种,但综合运用等价类划分、边界值分析和错误推测方法,可以满足绝大多数软件测试用例设计的需求。
再次,“好的”测试用例在设计时,需要从软件功能需求出发,全面地、无遗漏地识别出测试需求至关重要。
最后,如果想设计一个“好的”测试用例,你必须要深入理解被测软件的架构设计,深入软件内部的处理逻辑,需求覆盖率和代码覆盖率这两个指标可以帮你衡量测试执行的完备性。
03 | 什么是单元测试?如何做好单元测试?
- 代码要做到功能逻辑正确,必须做到分类正确并且完备无遗漏,同时每个分类的处理逻辑必须正确;
- 单元测试是对软件中的最小可测试单元在与软件其他部分相隔离的情况下进行的代码级测试;
- 桩代码起到了隔离和补齐的作用,使被测代码能够独立编译、链接,并运行。
04 | 为什么要做自动化测试?什么样的项目适合做自动化测试?
自动化测试是,把人工对软件的测试转化为由机器执行测试行为的一种实践,可以把测试工程师从机械重复的测试工作中解脱出来,将更多的精力放在新功能的测试和更全面的测试用例设计上。
然而自动化测试试一把“双刃剑”,虽然它可以从一定程度上解放测试工程师的劳动力,完成一些人工无法实现的测试,但并不适用于所有的测试场景,如果维护自动化测试的代价高过了节省的测试成本,那么在这样的项目中推进自动化测试就会得不偿失。
06 | 你真的懂测试覆盖率吗?
这章以后要多看几遍,了解的还是有很大的欠缺 。
测试覆盖率通常被用来衡量测试的充分性和完整性,从广义的角度来讲,测试覆盖率主要分为两大类,一类是面向项目的需求覆盖率,另一类是更偏向技术的代码覆盖率。
需求覆盖率
需求覆盖率是指测试对需求的覆盖程度,通常的做法是将每一条分解后的软件需求和对应的测试建立一对多的映射关系,最终目标是保证测试可以覆盖每个需求,以保证软件产品的质量。
代码覆盖率
简单来说,代码覆盖率是指,至少被执行了一次的条目数占整个条目数的百分比。
测试覆盖率通常被用来衡量测试的充分性和完整性,包括面向项目的需求覆盖率和更偏向技术的代码覆盖率。而需求覆盖率的统计方式不再适用于现在的敏捷开发模式,所以现在谈到测试覆盖率,大多是指代码覆盖率。
但是,高的代码覆盖率不一定能保证软件的质量,因为代码覆盖率是基于现有代码,无法发现那些“未考虑某些输入”以及“未处理某些情况”形成的缺陷。
另外,对于代码覆盖率的统计工具,我希望你不仅仅是会用的层次,而是能够理解它们的原理,知其然知其所以然,才能更好地利用这些工具完成你的测试工作。
07 | 如何高效填写软件缺陷报告?
具体看文章比较好,讲的很详细。
缺陷报告是测试工程师与开发工程师交流沟通的重要桥梁,也是测试工程师日常工作的重要输出。
一份高效的软件缺陷报告,应该包括缺陷标题、缺陷概述、缺陷影响、环境配置、前置条件、缺陷重现步骤、期望结果和实际结果、优先级和严重程度、变通方案、根原因分析,以及附件这几大部分。
缺陷报告的每一部分内容,都会因为目的、表现形式有各自的侧重点,所以想要写出一份高效的软件缺陷报告,需要对其组成有深入的理解。
08 | 以终为始,如何才能做好测试计划?
需要的时候看原文,写的很详细。
软件测试同软件项目一样,也要制定详细的测试计划。虽然在敏捷开发模式下,软件测试不再局限于厚重的、正式的计划文档,但是测试计划的重要性丝毫没有发生变化。
一份成功的测试计划,必须清楚地描述:测试范围、测试策略、测试资源、测试进度和测试风险预估这五个最重要的方面。
测试范围需要明确“测什么”和“不测什么”;测试策略需要明确“先测什么后测什么”和“如何来测”;测试资源需要明确“谁来测”和“在哪里测”;测试进度是需要明确各类测试的开始时间,所需工作量和预计完成时间;测试风险预估是需要明确如何有效应对各种潜在的变化。
09 | 软件测试工程师的核心竞争力是什么?
我把测试工程师按照工作内容,分为了功能测试工程师(即传统测试工程师)和测试开发工程师两类,分别给你分享了他们的核心竞争力。
对于功能测试工程师来说,其核心竞争力包括:测试策略设计能力、测试用例设计能力、快速学习能力、探索性测试思维、缺陷分析能力、自动化测试技术和良好的沟通能力这七大部分,你可以有针对性地提升自己某方面的能力,去获取更大发展空间的“敲门砖”。
而对于测试开发工程师来说,你需要具备优秀的测试系统需求分析能力和完备的知识体系,这样才能保证你设计的测试工作和平台,可以更好地满足提升测试效率的要求。
11 | 互联网产品的测试策略应该如何设计?
传统软件通常采用金字塔模型的测试策略,而现今的互联网产品往往采用菱形模型。菱形模型有以下四个关键点:
- 以中间层的 API 测试为重点做全面的测试。
- 轻量级的 GUI 测试,只覆盖最核心直接影响主营业务流程的 E2E 场景。
- 最上层的 GUI 测试通常利用探索式测试思维,以人工测试的方式发现尽可能多的潜在问题。
- 单元测试采用“分而治之”的思想,只对那些相对稳定并且核心的服务和模块开展全面的单元测试,而应用层或者上层业务只会做少量的单元测试。
12 | 从0到1:你的第一个GUI自动化测试
第一,Selenium 1.0 的工作原理
- 测试用例通过基于不同语言的 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 的工作原理
- 当使用 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。
13 | 效率为王:脚本与数据的解耦 + Page Object模型
“测试脚本和数据解耦”的本质是实现了数据驱动的测试,让操作相同但是数据不同的测试可以通过同一套自动化测试脚本来实现,只是在每次测试执行时提供不同的测试输入数据。
“页面对象模型”的核心理念是,以页面为单位来封装页面上的控件以及控件的部分操作。而测试用例使用页面对象来完成具体的界面操作。
16 | 脑洞大开:GUI测试还能这么玩(Page Code Gen + Data Gen + Headless)?
页面对象模型,是以 Web 页面为单位来封装页面上的控件以及控件的部分操作,而测试用例基于页面对象完成具体操作。最典型的模式就是:XXXPage.YYYComponent.ZZZOperation。
- 对于页面对象自动生成,商用测试软件已经实现了这个功能。但是,如果你选择开源测试框架,就需要自己实现这个功能了。
- GUI 测试数据自动生成,主要是基于测试输入数据的类型以及对应的自定义规则库实现的,并且对于多个测试输入数据,可以基于笛卡尔积来自动组合出完整的测试用例集合。
- 对于无头浏览器,你可以把它简单地想象成运行在内存中的浏览器,它拥有完整的浏览器内核。与普通浏览器最大的不同是,它在执行过程中看不到运行的界面。目前,Headless Chrome 结合 Puppeteer 是最先进的无头浏览器方案,如果感兴趣,你可以下载试用。
17丨精益求精:聊聊提高GUI测试稳定性的关键技术
- 对于非预计的弹出对话框引起的不稳定,可以引入“异常场景恢复模式”来解决。
- 对于页面控件属性的细微变化造成的不稳定,可以使用“组合属性”定位控件,并且可以通过“模糊匹配技术”提高定位识别率。
- 对于 A/B 测试带来的不稳定,需要在测试用例脚本中做分支处理,并且需要脚本做到正确识别出不同的分支。
- 对于随机的页面延迟造成的不稳定,可以引入重试机制,重试可以是步骤级别的,也可以是页面级别的,甚至是业务流程级别的。
- 对于测试数据引起的不稳定,我在这里没有详细展开,留到后续的测试数据准备系列文章中做专门介绍。
25 | 不破不立:掌握代码级测试的基本理念与方法
代码级测试的测试方法一定是一套测试方法的集合,而不是一个测试方法。
代码级测试方法主要分为两大类,分别是静态方法和动态方法。静态方法,顾名思义就是在不实际执行代码的基础上发现代码缺陷的方法,又可以进一步细分为人工静态方法和自动静态方法;动态方法是指,通过实际执行代码发现代码中潜在缺陷的方法,同样可以进一步细分为人工动态方法和自动动态方法。
人工静态方法,本质上通过开发人员代码走查、结对编程、同行评审来完成的,理论上可以发现所有的代码错误,但也因为其对“测试人员”的过渡依赖,局限性非常大;自动静态方法,主要的手段是代码静态扫描,可以发现语法特征错误、边界行为特征错误和经验特征错误这三类“有特征”的错误;人工动态方法,就是传统意义上的单元测试,是发现算法错误和部分算法错误的最佳方式;自动动态方法,其实就是自动化的边界测试,主要覆盖边界行为特征错误。
无非就是用驱动代码去调用被测函数,并根据代码的功能逻辑选择必要的输入数据的组合,然后验证执行被测函数后得到的结果是否符合预期。
28 | 带你一起解读不同视角的软件性能与性能指标
对于不同类型的系统,软件性能的关注点各不相同,比如:Web 类应用和手机端应用,一般以终端用户感受到的端到端的响应时间来描述系统的性能;非交互式的应用,比如典型的电信和银行后台处理系统,响应时间关注更多的是事件处理的速度,以及单位时间的事件吞吐量。
一个优秀的性能测试工程师,一般需要具有以下技能:性能需求的总结和抽象能力;根据性能测试目标,精准的性能测试场景设计和计算能力;性能测试场景和性能测试脚本的开发和执行能力;测试性能报告的分析解读能力;性能瓶颈的快速排查和定位能力;性能测试数据的设计和实现能力;面对互联网产品,全链路压测的设计与执行能力,能够和系统架构师一起处理流量标记、影子数据库等的技术设计能力;深入理解性能测试工具的内部实现原理,当性能测试工具有限制时,可以进行扩展二次开发;极其宽广的知识面,既要有“面”的知识,比如系统架构、存储架构、网络架构等全局的知识,还要有大量“点”的知识积累,比如数据库 SQL 语句的执行计划调优、JVM 垃圾回收(GC)机制、多线程常见问题等等。
目前,获取用户行为模式的方法,主要分为两种:
对于已经上线的系统来说,往往采用系统日志分析法获取用户行为统计和峰值并发量等重要信息;而对于未上线的全新系统来说,通常的做法是参考行业中类似系统的统计信息来建模,然后分析。
29 | 聊聊性能测试的基本方法与应用领域
当系统并发用户数较少时,系统的吞吐量也低,系统处于空闲状态,我们往往把这个阶段称为 “空闲区间”。
当系统整体负载并不是很大时,随着系统并发用户数的增长,系统的吞吐量也会随之呈线性增长,我们往往把这个阶段称为 “线性增长区间”。
随着系统并发用户数的进一步增长,系统的处理能力逐渐趋于饱和,因此每个用户的响应时间会逐渐变长。相应地,系统的整体吞吐量并不会随着并发用户数的增长而继续呈线性增长。我们往往把这个阶段称为系统的“拐点”。
随着系统并发用户数的增长,系统处理能力达到过饱和状态。此时,如果继续增加并发用户数,最终所有用户的响应时间会变得无限长。相应地,系统的整体吞吐量会降为零,系统处于被压垮的状态。我们往往把这个阶段称为“过饱和区间”。
30 | 工欲善其事必先利其器:后端性能测试工具原理与行业常用工具简介
完整的后端性能测试应该包括性能需求获取、性能场景设计、性能测试脚本开发、性能场景实现、性能测试执行、性能结果报告分析、性能优化和再验证。
33 | 无实例无真相:基于LoadRunner实现企业级服务器端性能测试的实践(下)
验证脚本的正确性
以单用户的方式,在有思考时间的情况下执行脚本,确保脚本能够顺利执行,并且验证脚本行为以及执行结果是否正确;
以单用户的方式,在思考时间为零的情况下执行脚本,确保脚本能够顺利执行,并且验证脚本行为以及执行结果是否正确;
以并发用户的方式,在有思考时间的情况下执行脚本,确保脚本能够顺利执行,并且验证脚本行为以及执行结果是否正确;
以并发用户的方式,在思考时间为零的情况下执行脚本,确保脚本能够顺利执行,并且验证脚本行为以及执行结果是否正确。
34 | 站在巨人的肩膀:企业级实际性能测试案例与经验分享
性能基准测试;稳定性测试;并发测试;容量规划测试。
性能基准测试,可以保证新发布系统的整体性能不会下降;稳定性测试,主要通过长时间模拟被测系统的测试负载,观察系统在长期运行过程是否存在问题;并发测试,往往被当作功能测试的补充去发现多线程、资源竞争、资源死锁之类的问题。容量规划测试,主要用于确定给定负载下的系统集群规模,其测试结果可以被用作系统容量设计的依据。