测试框架搭建理论
测试框架搭建
测试框架好比工具箱,其作用是可以便捷、高效地完成测试工作。而测试框架的引入,往往不是一蹴而就的,好的测试框架都是在实践中逐渐演化而来的。
自动化测试框架的构成
一个成熟的测试框架主要由四部分组成:基础模块、管理模块、运行模块和统计模块。
1. 基础模块
如果把自动化测试框架比作一辆汽车,那么自动化测试基础模块就是那四只轮胎,没有它们,这辆汽车寸步难行,它们一般包括如下部分。
- 底层核心驱动库: 一般指用于操作被测试应用程序的第三方库,例如在 Web 端的 Selenium/WebDriver。
- 可重用的组件: 一般用来降低开发成本,常见的有时间处理模块、登录模块等。
- 对象库: 存储被测试对象的仓库。在实际应用中,常常将页面进行分组,把一个页面上的所有对象放到一个类里,也就是 Page Object 模式。
- 配置文件: 包括测试环境的配置和应用程序的配置。
- 测试环境配置,指的是一个功能从开发代码完成到上线,往往要经过几个测试环境的测试,测试环境的配置能够减少环境切换成本;
- 应用程序配置,主要包括被测试程序的一些配置,利用配置文件,可以做到在不更改代码的情况下覆盖相同程序的不同程序配置。
2. 管理模块
自动化测试管理模块就好比汽车的内饰和外观,它对测试框架的使用操作体验有着直接影响,一般可分为测试数据管理和测试文件管理两部分。
- 测试数据管理
- 测试数据存放的文件是否跟测试用例强绑定,以及测试数据是否容易替换、是否和测试框架耦合等,这些都决定着测试框架的“内饰”好坏。
- 测试数据,一般指测试用例用到的各种测试数据,它们是为了验证业务正确性而构造的,每一条测试用例一般对应着一组或多组测试数据,测试数据创建一般分为实时创建和事先创建。
- 实时创建, 是在测试代码运行时才生成的测试数据。其好处有:测试数据是和测试代码耦合的,测试人员不需要关心其创建过程和业务调用链,通常用在测试的公用功能上。例如,给用户绑定银行卡以方便后续支付等。而坏处则是如果调用链太长,耗时会比较久。
- 事先创建, 是指测试代码运行前就准备好数据文件。其好处是数据拿来即用,几乎不耗费时间,由于没有业务调用,所以这在一定程度上减少了调用失败的风险;坏处则是数据文件本身需要维护,以保持可用性和正确性。
- 测试数据在测试中非常重要,它关系到你的测试是否有效,测试框架要做到对测试数据有效管理。
- 测试文件管理就好比汽车的颜色和外观,决定着第一印象,所以测试框架的文件结构应该清晰有序、一目了然。
- 比如,一个测试用例应该对应建立三个文件,分别是:Page 类文件(xxxPage,根据 PO 模型)、测试类文件(testXxxPage)和对象库文件(xxxPageYml)。
- 这三个文件共同描述了一个完整的测试用例,当你看到一个 Page 类时,就应该做到它还有一个对应的测试类。
- 测试文件的结构清晰有助于他人理解测试框架的设计思想,更有利于测试框架的维护和推广。
3. 运行模块
自动化测试运行模块是测试框架的发动机,它主要用于测试用例的组织和运行,一般包括如下部分。
- 测试用例调度,驱动机制。 测试框架应能按需组织,调度测试用例生成、执行。举例来说,测试框架可以在运行时根据使用者给定的 Tag 动态挑选要运行的测试用例,并把它们调度执行(可以顺序执行,也可以并发执行,还可以远程执行)。
- 错误恢复机制。 由于测试环境、测试程序、测试代码存在各种不确定因素,测试框架应该具备一定的错误恢复机制。在测试用例执行中,引起错误的类型一般可分为代码/运行导致的错误和环境/依赖导致的错误,测试框架应该能够识别这两种错误并给予相应的处理。
- 持续集成支持。 测试框架应该能够和 CI 系统低成本集成,包括通过用户输入参数指定运行环境、测试结束后自动生成测试报告等。
4. 统计模块
自动化测试统计模块,就相当于汽车的品质和口碑。好的统计模块,不仅能告诉你当前的测试有没有 Bug,还能分析软件质量随着时间的演变情况,这是测试框架的质量体现。
自动化测试统计模块一般包括如下两部分:
- 测试报告。 测试报告应该全面,包括测试用例条数统计、测试用例成功/失败百分比、测试用例总执行时间等总体信息。其中,对于单条测试用例,还应该包括测试用例 ID、测试用例运行结果、测试用例运行时间、测试用例所属模块、测试失败时刻系统截图、测试的日志等信息。
- 日志模块。 测试框架应该包括完善的日志文件,方便出错时进行排查和定位。
常用的测试框架类型
测试框架有很多类型,比较常见的有以下四类。
1. 模块化测试框架
模块化测试框架是利用OOP思想和PO模式改造而来的框架。
模块化测试框架把整个测试分为多个模块,模块化有以下几个特征:
- 我们将一个业务或者一个页面成为一个 Page 对象;
- 这个 Page 对象,我们以一个 Page 类来表示它;
- 这个 Page 类里存放有所有这个 Page所属的页面对象、元素操作;
- 页面对象和元素操作组成一个个的测试类方法,供测试用例层调用。
简单来说,使用了 PO 模式的框架就可以叫作模块化测试框架。
- 模块化测试框架的好处在于方便维护,你的测试用例可以由不同模块的不同对象组成;
- 坏处在于你需要非常了解你的系统及这些模块是如何划分的,才能在测试脚本里自如地使用,否则你就会陷入重复定义模块对象的循环里。
2. 数据驱动框架
数据驱动框架主要解决了测试数据的问题。
在测试中,我们常常需要为同一个测试逻辑,构造不同的测试数据以满足业务需求,这些测试数据可以保存在测试代码里,也可以保存在外部文件里(包括 Excel、File、DB)。
数据驱动框架的精髓在于,输入 M 组数据,框架会自动构造出 M 个测试用例,并在测试结果中把每一个测试用例的运行结果独立展示出来。
在 Python 架构里,最出名的数据驱动框架就是 DDT。
3. 关键字驱动框架
关键字驱动其实就是把一系列代码操作封装成一个关键字(这个关键字其实是函数名),在测试里,可以通过使用组合关键字的方式来生成测试用例,而不去关心这个关键字是如何运作的。
关键字的一个典型应用是将登录操作封装为关键字 Login,之后在后续代码里,有关 Login的操作,就仅需调用这个关键字 Login,而不必又重新进行一次登录操作。
关键字在领域里的最佳应用典范我认为是BDD(行为驱动开发),它甚至被当成一种独立的敏捷软件开发技术来使用。
4. 混合模型
需要注意的是,没有任何规定要求你的测试框架要属于以上某种类型,因为测试框架的存在不是为了分类型,而是为了更好地测试。
所以在工作中,我们常常需要糅合不同框架模型,我们将这种模式的测试框架称为混合模型。混合模型可以包含模块化框架,也可以使用数据驱动,或者使用 BDD 模式。
自动化测试框架设计原则
学习参考13条设计原则
- 清晰明了,学习成本低;
- 通用性强、可维护、可扩展;
- 对错误的处理能力强;
- 运行效率高且功能强大;
- 支持持续集成和版本控制。
清晰明了,学习成本低
自动化测试框架是个系统性工程,需要多成员一起运作,为了降低使用人员的学习成本,提升运行效率,自动化测试框架的代码、模块、报告应清晰明了。
1.代码规范
测试框架随着业务推进,必然会涉及代码的二次开发,所以代码编写应符合通用规范,代码命名符合业界标准,并且代码层次清晰。
特别在大型项目、多人协作型项目中,如果代码没有良好的规范,那么整个框架的代码会风格混杂、晦涩难懂,后续维护会很困难,最终成为没人敢动的“祖传代码”。
对应不同的语言则定制对应的不同的代码规范,比如:Java可遵循阿里巴巴开发手册进行制定代码规范,Python则已PEP8为准等
2. 模块清晰明确
模块化是将测试框架从逻辑上分为几个不同的模块,如下列的模块化分层的测试框架所示,可以根据实际情况自行裁剪。
模块化的好处是可重用,并且便于替换修改。
以上图为例,假设测试报告模块以前用的是 Allure,现在想替换成更加贴切自身业务的自研测试报告,我们仅需将报告模块替换掉就可以了。
但如果测试框架没有做模块化划分,测试报告是耦合在框架代码里的,那么就会导致无法切换测试报告,或者切换代价过大的问题,改动起来就会比较痛苦。
通用性强、可维护、可扩展
3. 通用性强
- 用于不同的操作系统,比如,测试框架不仅适用在 Windows 操作系统上,还要适用在 MacOS、Linux 系统上,越通用,测试框架的受众就会越多。
- 能解决同一类通用问题,比如,测试框架有个底层方法是用来操作弹出框的,那么无论是 Alert 框、确认框,还是一个允许用户输入的交互框,测试框架应该都能识别并操作。
4. 可维护、可扩展
- 可维护性
- 测试框架要做到容易维护,就一定要代码规范,模块清晰,除此之外整个测试框架代码风格还应该统一、易读、易懂。总之,要做到框架出问题时能容易定位并修改;更要做到,即使多人合作这个框架,这个框架代码要看起来是出自同一人之手。
- 可维护性无法用具体的指标衡量,也没有标准的实现方式。但不可维护性是可以感知的,因为不可维护性常常以代码逻辑混乱,不遵循编码规则等特征出现。所以一般通过消除不可维护性来证明测试框架是可维护的。不可维护的典型例子便是代码逻辑,比如一部分判断逻辑嵌套了非常多层的 if….else,就像上面的反例代码一样,这样的代码不易理解,改起来容易出错,这是你必须要避免的。
- 可扩展性
- 可扩展性指当需求变化时框架容易扩展。如果测试框架不能扩展,就无法解决业务发展带来的新问题,也就意味着测试框架的寿命会很短。
- 下面我举例说明下什么是可扩展性
- 假设测试框架运行测试的流程是:查找测试文件夹下的所有用例 → 判断该用例是否要运行 → 加入用例到待运行用例集 → 顺序运行测试用例 → 输出测试报告。
- 比如现在随着业务发展,我有了新需求: 需要按照一定的规则将“顺序运行”改为“并发运行”,即将带有特定标签的测试用例改为“并发运行”,而将没带有特定标签的测试用例继续保持“顺序运行”。
- 如果我们的测试框架可扩展,那么我仅需简单更改“顺序运行测试用例”这个模块的相关代码即可;反之,我则需要将测试流程重新设置甚至改造,所以我说可扩展性是测试框架的一个重点。
对错误的处理能力强
该原则是从测试运行的角度看的,当我们测试开始时,往往会运行很多测试用例,当测试出错时,测试框架如何处理才能让运行更有效率呢?
5. 错误处理机制,高效解决
在测试运行中,难免由于种种原因运行错误,这时测试框架就必须具备处理错误的能力。错误处理机制一般分为停止运行和错误恢复两种。
- 停止运行是指发现错误后直接停止本次测试,在实践中一般在测试框架本身出现错误的时候才会使用。
- 针对具体的测试用例执行,错误恢复这种方式比较常见。其步骤通常是标记当前用例为“失败”,清理失败数据,恢复测试环境,然后再运行下一条测试。其中,根据错误恢复的时机又可以分为事先恢复(当前用例运行前,将环境和数据恢复为初始状态)和事后恢复(当前用例执行完成后,将环境和数据恢复为初始状态)两种。事先恢复现在是比较常用的,因为事后恢复可能会因为用例执行失败而永远执行不到。
6. 系统日志清晰,方便调试
除了错误处理机制外,系统的操作日志也能帮你快速排查问题根源,所以平时的日志一定要清晰详细,最好具备上下文,这样才能根据日志进行有效调试,快速定位错误发生的原因。
对于测试框架来说,系统日志除了要按等级 DEBUG、INFO、WARN、ERROR 划分外,最好包括以下内容:
记录测试用例的开始和结束时间;
记录测试人员的关键操作(如写文件、连接 DB、更改 DB 等);
关键方法的异常信息(如 run 模块出错部分的上下文信息等)。
运行效率高且功能强大
在当前的互联网大环境下,每时每刻都可能有构建(Build)发生,有了构建就需要不断地测试,那么运行效率的高低直接决定了构建和发布的次数多少。
7. 支持测试环境切换
一个产品从开发到上线,会经历几个测试环境的测试,比如 dev 环境, 集成测试环境,预生产环境,生成环境等。所以测试框架要能做到,一套脚本多环境运行,支持环境切换,并且能根据环境进行自动化的配置(包括系统配置、测试数据配置等)。
8.支持外部数据驱动
- 根据外部输入数据,动态生成测试用例,并在测试报告中单独展示。测试框架会把这些只有数据不同,步骤和操作都相同的测试用例,在运行中解析成一个个不同的独立测试用例,并在测试运行结束后,全部逐一展示到测试报告里。
- 根据外部输入数据,动态切换运行用例。测试目的不同,其需要采用的测试用例也会不同,所以自动化测试框架会给各个测试用例打上标签,再根据需要,自动选择具备特定标签的测试用例进行运行。
9.支顺序、并发、远程运行
当你的测试用例有上千条,甚至上万条时,顺序测试会花费大量的时间。为了快速得到测试结果,测试框架应该支持顺序、并发、远程执行,这样能够缩短测试用例的整体执行时间。
10.报告完备详尽
测试报告是 QA 工作中的重要一环,通常在一个项目结束或者一个 sprint 结束时发出。完备详尽的测试报告,不仅可以述说 QA 到底做了哪些工作,还可以看出整个项目的生命周期运行得平稳与否,软件的质量如何。
11. 解决当前没有解决的问题
“不要重复造轮子”是工具创造的首要原则。从功能角度看,框架得到认可,要么是解决了当前无法解决的问题,要么是解决方案比当下的更好。
例如,Selenium/WebDriver 最开始为人所知是因为它开源、可跨平台;后来 Selenium/WebDriver 的替代者 Cypress 为人所知,是因为它还具备运行在浏览器之内,且自备 Mock 的能力。
所以,你的框架能不能被认可,就在于它是否具有独特的功能特性,这是与其他框架区别开来的标签,也是弥补市场空白的撒手锏。
支持版本控制和持续集成
版本控制可以让使用者更好地理解框架的演变历史;框架支持持续集成可以让框架迅速融入公司的技术体系中,使框架被越来越多的团队接纳。
12.版本控制,回溯复盘
什么是版本控制?其实就是将代码纳入版本控制系统(如 Git)的管理之下。那么为什么测试框架要做版本控制呢?请思考如下问题:
你开发了功能 A,老板说这个功能不要,你就把 A 代码删除了。等一个月后业务发生了变化,功能 A 又变得需要了,如果没有版本控制,你怎么把 A 代码恢复回来?
我们知道,当前的测试开发中,一个人单打独斗的情形很少见了,常见的是团队协作开发。那么假设你和 B 在开发不同的功能,但是都改动到了同一个底层共享模块。那么如果没有版本控制,你们的代码提交后还能正常工作吗?
假设有了版本控制,那么这些问题发生后,复盘时就非常容易找到根本原因,代码回溯也很方便,所以测试框架应该支持版本控制。
此外,还有一个用处就是对测试代码进行版本控制。假设你同时需要支持同一个微服务的两个不同版本的业务测试。有了版本控制,你的不同版本的测试代码就能以不同分支的形式出现,否则,你只能一次保持一个版本的代码,非常不方便。
有了版本控制,不仅协作开发、版本切换变得非常容易,使用者也可以通过查看版本之间的变化来理解框架的发展脉络。
13.持续集成,全局出发
前面的原则是从测试本身角度出发的,而“持续集成”是从整个公司业务出发,需要你与整个开发团队合作完成,同时这是你晋级“资深”的体现。
测试框架应该能方便地集成至公司的持续集成系统,并且通过持续集成系统触发测试。
一般来讲,公司的持续集成和持续发布系统通常由 DevOps 和开发架构师打造,测试要做的就是将测试框架融入公司的持续集成和持续发布技术栈。
那么测试框架就应支持通过持续集成系统,触发测试用例运行。具体来说就是:当某个代码提交的 hook 被触发时,持续集成会打包并部署最新代码到测试机上,此时测试框架及其对应的测试用例应能被唤醒并执行。
支持持续集成的程度决定了框架在团队和公司的接纳度。支持持续集成的成本越小,框架就越容易被推广和深度使用。
分层测试
“分层测试”是什么?
“分层测试”其实并不是一个专业名称,它只是国内互联网从业者约定俗成的一个叫法。它来自专业名称“Test Pyramid”,也就是我们常说的“测试金字塔”,是 Martin Fowler 在 2012 年提出的一个概念。
“测试金字塔”将软件测试分为不同的粒度,强调了不同粒度的自动化测试在整个自动化测试中的占比应该不同,旨在指导我们如何使用不同类型的自动化测试来实现软件测试价值的最大化。
它有如下原则:
分粒度来写自动化测试;
越是高层次,自动化测试的占比应该越少。
如图,越是底层的测试,比如单元测试(Unit Test),测试耗费的时间就越少,花费的成本就越小,越往上层,测试所需的时间就越多,成本就越高,在“测试金字塔”模型中,UI 测试是性价比最低的一个测试类型。所以,我们说“测试金字塔”模型揭露了测试速度、测试成本和自动化测试类型三者之间的关系。
它最开始只有 Unit、Service 和 UI 这三个粒度,这三个粒度像是把自动化测试分为了三个不同层次,所以行业内我们将它叫作 “分层测试”。
Unit 层(单元测试层)
单元测试层位于“测试金字塔”的最底层。主要关注函数,类级别的测试;单元测试之间相互没有依赖,是独立的,可重复执行的;单元测试的执行时间最短,成本最低;在实践中,大约有 70% 的测试用例都是单元测试。
Service 层(服务层)
服务层位于“测试金字塔”的中间层。主要关注模块本身,模块与模块集成的接口, 子系统本身, 各个子系统之间的测试;Server 层的测试可涉及框架、数据库、第三方服务等;在实践中,大约有 20% 的测试用例是测试。
UI 层
- UI 层位于“测试金字塔”的最上层。 关注从用户角度看, 整个系统的表现和交互;UI 层的测试通常通过操作页面对象来执行;耗时最长,成本最高。在实践中,UI 层的测试大约占比 10% 左右。
“分层测试”的发展
在实践中,“测试金字塔”逐渐更加细化,形成了如下的样子:
将原本的 Service 层进一步细分为组件测试(Component Test)、集成测试(Integration Test)和 API 接口测试(API Testing)。
而原本顶部的 UI 层,则被 E2E(End To End)测试取代,E2E 测试和 UI 测试的区别是:UI 测试的重点在于产品或系统的 UI 部分;E2E 则更关注整个产品或者系统的行为是否正确,显然 E2E 能更加准确地描述测试活动的重心。
此外,还在顶部另加了一个 Exploratory Test(探索性测试)。探索性测试不是随机测试,探索性测试一般会设定一个测试目标,然后根据测试执行者对系统的了解,从某一个点出发,围绕着测试目标,同时进行测试用例的设计和执行工作,当前探索性测试一般采用手工测试的方式来进行。
- Unit(单元测试)层: 由于测试的都是具体的方法和类。所以一般由开发自测。
- Component Test(组件测试): 这部分是 Unit 层的组装,多个 unit 组成一个 Component。对于一个组件来说,其输入可能是独立的,那么可由测试人员测试,也可能依赖别的组件提供,这时通常需要开发来提供 Mock。
- Integration Test(集成测试): 把多个 Component(组件)形成一个子系统或者系统,集成测试分自顶向下集成和自底向上集成,集成测试一般由测试人员来完成。
- API Test(接口测试): API 通常是指两个子系统直接通过 API 进行通信(当然不同模块间的通信也会通过 API 来进行),接口测试一般由测试人员来完成。
- E2E 测试: 关注系统的交互和 UI 的展现,通常由测试人员完成。
- 探索性测试: 由测试人员手工完成。
总而言之,“测试金字塔”模型指导我们在进行测试时, 应该投入大量精力到运行速度更快,成本更低的 Unit 测试(单元测试)中;应该投入一部分精力到 Server 测试中(即组件测试和API测试);在测试速度更慢,成本更高的 UI 层面的测试里,我们只需投入最小精力即可。
“分层测试”的误区
误区 1:分层测试一定是顺序的
实际上,分层测试并没有规定每一层测试的先后顺序,在实践中,每一层的测试是没有执行先后顺序的,是可以同时运行的。
误区 2:不能跨层执行测试
有的同学认为,既然分层分得这么清晰,是不是意味着不能在这一层执行其他层的测试呢?比如,不能在 Service 层进行 E2E 测试,同样也不能在 E2E 层调用 API。这是不对的。
分层测试并没有这样的限制。实际上跨层测试是很经常的事情,比如我们在 E2E 测试时调用接口来迅速构造数据,或者使用 Mock 绕过某些非目标测试场景。
还有,特别针对前端的验证来说,比如针对 UI 的验证,可以下沉到 Component 层(组件层)来尽早验证。举例来说,假设你的前端项目采用了 React、Vue、Spa 等 Web 技术,那么,利用这些框架提供的工具在 Component 层(组件层)针对 UI 进行测试是非常普遍的。
误区 3:分层后,单元测试越多越好,UI 测试越少越好。
答案也对也不对。对,是因为理论上越底层的测试发现问题的成本越低,我们应该多做单元测试;不对,是因为现实往往比理论更复杂。
举例来说,假设你的应用是一个跟第三方系统集成的项目(比如对接第三方支付接口)。那么因为第三方接口已经完成,在这个项目中,单元测试已经不用做,且不在你的掌握范围内。这时测试应该把关注点放在 E2E 层,以穷举业务场景的方式,来尽可能多地进行测试,以满足需求。
所以你需要根据项目,合理选择需要实施哪种层次的测试,这才是正确做法。
“分层测试”的最佳实施原则
1. 不要重复测试
重复测试是指,同样一个检查点,在 Unit 层有测试用例,在 Service 层也有测试用例,在 E2E 测试里也有覆盖。
在实践中,太多人尝试在每一层里尽可能穷尽所有功能的测试验证。这是不对的,理想的情况是,每一个层次的测试用例集合起来,正好是最小的,能覆盖所有需求的测试集。
重复测试坏处在于,如果有改动,那么就要改动 3 次,并且还增加了脚本维护时间,测试成本非常高。
2. 测试尽量下沉
测试尽量下沉,是指能在单元测试层覆盖的,尽量在单元测试层覆盖。测试下沉的好处是如果你的测试“失败”了,你清楚地知道哪行代码有问题;而如果 E2E 测试失败了,你要花费更多精力才能找到出错的代码行。
测试下沉并不意味着测试脚本写完就算了,它是一个动态的过程。举例来说,假设你发现某一条 E2E 测试发现了一个功能性 Bug,这意味着你的单元测试某处缺失。这时,你需要把针对这个 Bug 的检查下沉到单元测试层,并且删除掉 E2E 层的测试。
总之,你需要多写单元测试。
3. 根据业务特性,测试合理分层
测试合理分层有两个含义。
第一个就是合理选择分层模型。
- 比如如果是前端占比比较多的测试,你可能选择“奖杯模型”;如果是针对微服务的测试,你可能选择“纺锤模型”。
第二个是合理选择在哪一层编写你的测试用例。
- 假设你需要做一个用户交易历史分页展示的功能,你在单元测试时发现了一个边界值的问题——数据量大到分页超过 1000 页时,程序会出错。从用户的操作习惯看,数据量根本达不到 1000 页,那么你永远走不到 E2E 层这一步,此时你的测试应该放在单元测试层。相反,假设如果你的业务流程限定死了,这个分页不可能达到 1000 页,那么这个单元测试就存在“过量测试”的问题,应该从单元测试层移除,转而在 E2E 层根据业务逻辑编写测试用例。