AIGC在金融场景是如何落地的? 了解详情
写点什么

无论成败:Selenium 脚本在随机测试中的复用

  • 2017-04-25
  • 本文字数:6216 字

    阅读完需:约 20 分钟

主要结论

  • 测试自动化是一种妥善记录并具备清晰定义的方法,借此可以反复运行同一套测试脚本。然而与此同时,这种测试自动化脚本还可进一步实现其他更有创意的应用。
  • 虽然自动化的分析思维很难实现,但我们的脚本中无疑可以具备一定的随机性。
  • 测试中“随机性”的具体程度各异:从随机输入和参数,再到全面的随机测试用例,情况不一而足。
  • 很难将随机步骤与相应的验证措施匹配起来,但我们可以使用不同的验证策略确保应用程序能够按照预期工作。
  • 随机测试无法取代主观或传统测试技术,但可在回归测试过程中让我们对应用程序质量更为自信。

正如 Cem Kaner 在他的一片教程中所说探索式测试是一种强调个人自由度和个体测试人员责任的软件测试方式,可通过将与测试有关的学习、测试设计、测试执行,以及测试结果的理解视作一系列彼此提携,在项目完整过程中并行执行的活动,借此对测试工作的成果进行持续不断的优化。

简而言之,按照他的定义,众所周知的“软件质量和消费者(Software Quality and Consumer)”主张为测试人员提供了在项目中按照自己认为合适的方式进行测试的自由和责任。循序渐进地记录所有规范,这种做法已经不再是必须,原因也很简单,创意过程基本式无法记录的,对吧!在他在 TestBash 3 大会上有关测试中决策工作的演讲中,Mark Tomlinson 对系统的主观理解这一想法表示支持。如果将其作为探索式的,基于风险和基于会话的测试技术(可将其称之为主观技术)的核心,测试者将能主观地确定应用程序中可能导致失败的重要环节。

可以参看这张旋转舞者的动力学错觉示意图:不同时刻内,我们的大脑或判断舞者以一个特定的顺序旋转:向左或向右。测试工作也会面临类似情况:我们可能考虑使用不同流程实现相同结果,或相同流程导致虽不同但符合预期的结果,或者,嗯……任何其他结果。

整个测试执行过程所用的主观技术可以通过各种成熟的分析思维和“随机性”的优势加以引导。其中后者是一个更重要的要素,本文, 将揭露自动化测试中“随机化”的神秘面纱。

明确起见,测试自动化并不是一种创作活动,而是一种妥善记录且清晰定义的方法,借此可以让同一套测试脚本反复运行使用。问题在于,我们该如何使用这些测试自动化脚本,同时更更具创意?

产品质量随时间而变

产品质量模型和所记录的测试场景可通过特定的状态机以及外部特性加以概括。这一点正是测试自动化所热爱的。测试自动化所关注的正是根据一些非常具体的测试需求集编写测试脚本。

这种做法很适合功能性回归测试:清理、打磨、全新发布,随后由开发大师创建。姑且将其称之为 Shiny 吧。

但经过一段冗长、精疲力竭的开发时间线后(伴随着多次发布,长达数年的支持,数百个 Bug 的修复和功能请求等),系统会变成什么样?

确实,从用户接口的角度来看,可能非常类似于那种虽然老旧但依然工作良好的系统,但表面之下,这种情况通常被称之为“大泥球”。

对于这样的系统,就算使用自动化脚本,具体功能的哪些部分依然能获得和最初生产发布时同等程度的测试?也许只有 30%-80% 的部分可以吧。那么其他功能呢?不知道。

当然,此时最简单的办法可能是审查所有现有的质量文档,改良原有的场景,(即时)引入新的场景等。但考虑到业内的经验,随着遗留系统的规则测试文档逐渐过时,虽然更新工作依然重要,但这种做法并非总是可行。

为测试自动化解决方案打造妥善定义的架构

下图是一个精简的测试自动化解决方案的范例图,其中包含三层(类似于基于 UI、业务逻辑和数据库实现业务应用程序的方法):UI/API 映射、业务裸机,以及测试脚本。

  • UI/API 映射代表该解决方案的技术端:UI 自动化工具程度与自动化系统的 UI 高度绑定,这一层所用的方法可能类似于focus()type_text()click_button()
  • 业务逻辑是一种由来自业务操作的关键字组成的库。业务操作是指可以在应用程序中执行的某个步骤(如login()create_user()validate_user_created())。
  • 测试脚本负责执行一系列链再一起的业务步骤。

深入了解独立测试(Separate Test)

考虑这样一种简单的记录测试用例:执行这个 – 验证这个,执行那个 – 验证那个,执行某某 – 验证某某。合格的自动化开发者会创建一系列类似下面这样的方法:

复制代码
do_that(), verify_that(), do_this(), verify_this(), do_bla().

测试脚本会按照某种特定的顺序调用这样的方法:

复制代码
mySpecifiedCase_1(){
do_that();
verify_that();
do_this();
verify_this();
do_bla();
verify_that();
verify_this();
}

由于脚本没找到任何 Bug,我们在某些特定阶段的任务就变成让它查找潜在的系统问题。

随机化方法 1 – 裸随机

从业务角度来说,自动化解决方案中任何步骤都是有效的。因此探索式测试使得我们可以自由地在任何时间点执行任何步骤。这些步骤的混搭也很简单。我们需要在执行过少数几次测试后,遵循已实现步骤打造“随机”测试用例。

输入:解决方案中所有业务方法的数量,要生成的测试脚本数量,生成每个测试脚本所需步骤数量。

输出:类似于下列脚本:

复制代码
myRandomCase_1(){
do_that();
do_bla();
verify_this();
}

很明显,就算某些测试用例可能(甚至已经)成功运行,大部分依然会失败,因为大量用例实际上是在试图完成无效操作。如果还没执行过do_this(),那么verify_this()无疑会失败。

随机化方法 2 – 有先决条件的随机方法

这种方式的想法在于只有在工作流中已包含先觉步骤后,才向工作流中加入后续步骤,但这需要对代码库进行必要的扩充,确保测试案例生成器可以理解并保证准确的序列。为此可在方法之上添加特性或注解:

复制代码
@Reguires(do_this)
verify_this()
{…}

这样我们就得到了:

复制代码
myRandomCase_2(){
do_bla();
do_this();
verify_this(); //can be added, because prerequisite step is already in test
}

这是一种更可预测的方法。但如果do_this()verify_that()需要在同一个 Page1 上执行,而do_bla()已经到了 Page2 又该怎样?

此时我们面临一个新问题:verify_that()绝对会失败,因为无法找到执行所需的控制 / 上下文。

人工随机化方法 3 – 上下文感知

测试生成器必须了解执行位置上下文(例如 Web 开发中的“页面”)。当然,此时也可以通过特性 / 注解为生成器提供活跃上下文。

复制代码
@ReguiresContext(pageThis)
verify_this()
{…}
@ReguiresContext(pageThis)
do_this()
{…}
@ReguiresContext(pageThis)
@MovesContextTo(pageThat)
do_bla()
{…}

本例中do_this()verify_this()不会放在将上下文改为 pageThat 的方法,或上下文为 pageThat 的方法之后。

因此我们可以得到一个类似下面这样的测试脚本:

复制代码
myRandomCase_3(){
do_this();
do_bla();
do_that();
}

或者也可以通过方法链实现。假设业务方法返回的对象为页面,测试案例生成器会持续追踪执行“步骤”前后浏览器中显示的页面,因此可以确定需要调用验证或“步骤”方法的正确页面。这种方法需要额外检查以验证流程是否正确,但这个操作可以无须注解实现。

筛选恰当的用例

至此介绍的方法已经可以生成相当大量的测试用例。

主要问题在于,验证过程本身,以及验证失败的测试场景是否是应用程序内的 Bug,而非自动化测试脚本逻辑导致的,这些工作也需要耗费大量时间。

因此可以实现一种“预言”类,借此预测所获得的结果是否满意,或是否代表任何错误信息,并且必要时可进行后续分析。然而本例我们选择了一个略微不同的方法。

可以通过下列这一套规则代表应用程序的失败是 Bug 引起的:

  • 500 错误或类似页面
  • JavaScript 错误
  • “未知错误”或因为误用造成的类似的错误信息
  • 应用程序日志中有关异常和 / 或错误情况的信息
  • 发现与任何其他产品有关的错误

本例中,可在每个步骤执行完毕后验证应用程序状态。因此自动生成的脚本看起来是这样的:

复制代码
myRandomCase_3(){
do_this();
validate_standard_rules();
do_bla();
validate_standard_rules();
do_that();
validate_standard_rules();
}

其中validate_standard_rules()方法可以搜索上文提到的各种问题。

注意:通过与 OOP 结合,这种方法会显得更为强大,可以检测出实际的 Bug。在 Page Object 超类实现常规检查需要查找“常规问题”,例如 JavaScript 错误、日志中的应用程序错误等。对于与特定页面有关的合理检查,可以绕过这种方法额外增加针对具体页面的检查。

实验

为了进行实验,我们决定使用公开的邮件系统。考虑到 Gmail 和 Yahoo 的流行度,这些系统中所有存在的 Bug 都已被发现的可能性相当高。因此我们选择了 ProtonMail

Taking Over Random

假设自动化解决方案已经就位,我们“采用”了 Shiny 系统的自动化测试机制:首先建立一个通用的 Java/ Selenium 测试项目,其中包含几个使用 Page Object 模式实现的冒烟测试。随后按照最佳实践,所有业务方法可以返回一个新的 Page Object(针对业务方法结束时依然显示在浏览器中的页面)或当前 Page Object,除非页面被更改。

为进行自动化探索式测试,我们增加了包含在explr.core包中的类,其中最感兴趣的当属TestCaseGeneratorTesCaseExecutor

TestCaseGenerator

为了生成新的“随机”测试用例,可以通过TestCaseGenerator类调用两个generateTestCase方法之一。这两个方法都能以参数的方式接受代表所生成测试用例中“步骤验证对”数量的整数。第二个方法还可额外接受一个代表要使用的“验证策略”数量的参数(第一个方法使用默认策略,本例为 USE_PAGE_SANITY_VERIFICATIONS)。

验证策略代表在向测试用例添加“检查”步骤时所用的方法。目前我们有两个选项:

  • USE_RANDOM_VERIFICATIONS:第一个,同时也是最明显的策略。该策略的想法在于,使用来自页对象的当前验证方法。但不足之处在于严重依赖上下文。例如:我们随机选择了一个方法来验证特定主题的消息是否存在。首先,我们必须知道要查找哪个主题。为此我们引入了@Default注解和DefaultTestData类。DefaultTestData包含的常规测试数据可用于随机测试。@Default注解可用于将该数据绑定给特定的方法参数。随后我们需要确保包含该主题的消息先于验证操作就已存在(可在执行该规范的过程中,或之前的任何测试过程中创建)。为此可通过@Depends注解告诉TestCaseGenerator检查特定方法的调用,如果当前步骤之前没找到则直接添加。此外我们还需要确保消息没有在验证之前删除。我们发现对于生成的测试用例,依赖性问题大幅降低了随机化程度,并且这种方法的稳定性也无法满足要求。
  • USE_PAGE_SANITY_VERIFICATIONS:该策略可检查显而易见的应用程序失败,如显示了错误的页,错误信息,JavaScript 错误,应用程序日志中的错误等。在依赖性方面这个策略更灵活,可在需要时实现针对具体页的检查,例如已经足够灵活到可以找出实际的 Bug。目前我们将其用作默认的验证策略。

TestCaseGenerator类可按照类名搜索 Page 对象:每个名称中包含“Page”字符串的类都会被看作是页对象。页对象的所有公开方法会被视作业务方法。名称包含“Verify”字符串的业务方法会被视作验证,所有其他方法会被视作测试步骤。@IgnoreInRandomTesting注解可用于从列表中排除某些工具方法或整个页对象。

随后可从两个列表中随机选择方法生成测试用例:一个列表包含测试步骤,一个列表包含验证步骤(如果所选验证策略需要验证步骤的话)。选择第一个方法后,将检查其返回值是否为另一个页对象。如果返回值是另一个页对象,那么将从其方法中选择下一个步骤(参见上文备注)。为避免在两个页之间循环往复,有一成的概率会跳转至一个完全随机的页面。如果方法使用@Depends注解标注了任何依赖项,则会按需解决这些问题并添加。

为避免出现从当前所显示页之外其他对象调用测试方法的情况,生成的测试用例会传递一个额外的验证,借此添加缺少的导航调用。

TesCaseExecutor

生成之后,测试用例基本上就是一种“类 - 方法对”列表,可通过特定方式执行或保存。尽管可在运行时执行,但从调试和后续分析的角度来看,保存为文件是一种更好的做法。

生成的测试用例可通过多种方式执行,可以TesCaseExecutor作为其接口,以SaveToFileExecutor作为唯一的实现,借此可简单地创建一个代表所生成测试用例的.java 文件。令人惊异的是,这种相当简单的解决方案完全满足了我们的需求:实现速度快,可对测试结果进行深入分析,并能了解具体的生成方式。唯一的不足在于,必须手工编译并运行生成的测试用例,不过对于实验来说,这也算不得什么大问题。

SaveToFileExecutor生成的测试用例代码可通过模板转换为可编译的文件。这样生成的测试范例如下:

复制代码
@Test(dataProvider = "WebDriverProvider")
public void test(WebDriver driver){
login(driver);
//****<Generated>****
ContactsPage contactspage = new ContactsPage(driver, true);
InboxMailPage inboxmailpage = contactspage.inbox();
inboxmailpage.sanityCheck();
ComposeMailPage composemailpage = inboxmailpage.compose();
composemailpage.sanityCheck();
composemailpage.setTo("me@myself.com");
composemailpage.send();
inboxmailpage.sanityCheck();
List list = inboxmailpage.findBySubject("Seen that?");
inboxmailpage.sanityCheck();
inboxmailpage.inbox();
inboxmailpage.sanityCheck();
DraftsMailPage draftsmailpage = inboxmailpage.drafts();
draftsmailpage.sanityCheck();
inboxmailpage.inbox();
inboxmailpage.sanityCheck();
inboxmailpage.sendNewMessageToMe();
inboxmailpage.setMessagesStarred(true, "autotest", "Seen that?");
inboxmailpage.sanityCheck();
TrashMailPage trashmailpage = inboxmailpage.trash();
trashmailpage.sanityCheck();
//****</Generated>****
}

SaveToFileExecutor生成的代码位于<Generated>备注之间,其余代码由模板添加。

从所执行的操作方面来看,我们生成的用例多样化程度一般,但只要添加包含更多测试步骤的更多页对象即可轻松解决。

在进行过上千个“随机”测试后,我们发现 Protonmail 没什么大问题(例如错误页),但浏览器汇报了一些 JavaScript 错误,对于依赖 JavaScript 进行邮件编解码工作的系统,这些问题非常重要。很明显,整个实验中我们并不能访问服务器日志,但就实验的角度来说,已经足够展示出这样的方法对被测试系统质量的促进能起到多大的作用。

当然,随机测试无法取代主观或传统测试技术,但可在回归测试过程中让我们对应用程序质量更为自信。

关于本文作者

** Oleksandr Reminnyi ** 是 SoftServe 的测试自动化专家,在软件开发领域有着长达 12 年的从业经验,他擅长将不同 IT 领域的知识结合在一起为任何问题提供最佳解决方案。Oleksandr 曾在乌克兰和很多国际会议,例如 Atlassian Summit、ITWeekend、HotCode、TC World、Information Energy Netherlands,以及 SQA Days 上发言。Oleksandr 在本文中介绍了开发、测试自动化、常见错误,以及问题解决模式等方面的内容。

** Pavlo Vedilin ** 是 SoftServe 的测试自动化专家,在 IT 领域有 8 年的工作经验,过去 6 年来,他一直在从事有关 QA、JEE 开发者,以及测试自动化和 UI 脚本等方面的工作。Pavlo 曾从事过有关软件工程、Web 服务、安全软件等项目,为不同类型的测试和 UI 脚本提供了大量测试自动化工具。Pavlo 认为自动化即有趣又简单,可以取代计算机领域大量例行的无聊任务。

作者 Oleksandr Reminnyi Pavlo Vedilin 阅读英文原文 Hit or Miss: Reusing Selenium Scripts in Random Testing

2017-04-25 17:191764
用户头像

发布了 283 篇内容, 共 99.1 次阅读, 收获喜欢 58 次。

关注

评论

发布
暂无评论
发现更多内容

群晖(Synology)NAS 安装 MongoDB

HoneyMoose

加密标准中DES与AES到底是什么?两者有啥区别?

wljslmz

信息安全 加密 AES 10月月更 DES

CentOS 上安装 Sonatype Nexus 仓库

HoneyMoose

2022-10-20:以下go语言代码输出什么?A:7;B:7.0;C:0;D:编译错误。 package main import ( “fmt“ ) func main() { const

福大大架构师每日一题

golang 福大大 选择题

Git 操作命令笔记

宇宙之一粟

git 10月月更

Sonatype Nexus 管理员初始密码

HoneyMoose

“程”风破浪的开发者|慢慢踏上算法学习之旅

Aion

学习方法 算法 10月月更 “程”风破浪的开发者

“程”风破浪的开发者|代码规范

over℡

学习方法 “程”风破浪的开发者

智能化运维场景分析

阿泽🧸

10月月更 智能化运维

PHP出发(php+apache+MySQL)

江拥羡橙

php MySQL apache 社区 10月月更

Fabric8 Docker Maven Plugin 如何让部署的时候执行 Docker 打包推送

HoneyMoose

CentOS部署Harbor镜像仓库

程序员欣宸

Docker 10月月更 habor

【设计模式】Java 语言不同的编程范式-第1章

跟着飞哥学编程

设计模式 编程范式 java 编程 10月月更

微信小程序云开发收费调整,大家怎么看?

江拥羡橙

微信小程序 云开发 10月月更

golang中的切片

六月的

Go slice

SAP | 内部表的表类型

暮春零贰

SAP 10月月更 内部表

数据开发也能双轮驱动?

乌龟哥哥

10月月更

“程”风破浪的开发者|如何更好的学习专业知识

海风极客

学习方法 10月月更 “程”风破浪的开发者

【愚公系列】2022年10月 Go教学课程 035-接口和继承和转换与空接口

愚公搬代码

10月月更

【一Go到底】第二十一天---defer

指剑

Go golang 10月月更

Spring Batch 中的 chunk

HoneyMoose

【资损】资损防控的系统规范-收单类服务设计

小明Java问道之路

架构 安全 金融 10月月更 资损

Qt|模态窗口如何实现进入页面等待加载数据效果

中国好公民st

c++ qt 10月月更

Vue组件入门(十一)$attrs

Augus

Vue 3 10月月更

【资损】资损防控的系统规范-渠道网关类设计

小明Java问道之路

架构 安全 金融 10月月更 资损

GaussDB(DWS)如何实现实时,批量和交付式查询一站式开发

乌龟哥哥

10月月更

数据湖(六):Hudi与Flink整合

Lansonli

10月月更 Hudi与Flink整合

React源码中的dom-diff

夏天的味道123

React

YARN基本架构

穿过生命散发芬芳

YARN 10月月更

el-table表格还可以这么玩

江拥羡橙

Vue 3 Element UI 10月月更

Maven docker-maven-plugin 插件 Push 413 错误

HoneyMoose

  • 扫码添加小助手
    领取最新资料包
无论成败:Selenium脚本在随机测试中的复用_文化 & 方法_Oleksandr Reminnyi_InfoQ精选文章