随着时间的流逝,对任何产品进行更改变得更加困难,并且不仅释放新功能,而且破坏旧功能的风险也越来越大。 通常,他们尝试自动执行此过程,而不是手动检查整个项目。 如果您与正在测试界面并在会议中四处走动的人交谈,那么很明显,在Web测试Selenium规则世界中,绝大多数使用Page Object作为代码组织。
但是对我来说,作为一名程序员,由于某种原因,我不喜欢在不同团队中看到的这种模式和代码-SOLID字样在我的脑海中响起。 但是我已经准备好接受这样一个事实,即由于缺少替代方法(例如大约一年前),在Angular Connect上测试人员可以按自己的意愿编写代码,所以我听到了一份关于使用Screenplay模式测试Angular应用程序的报告 。 现在我要分享。

实验兔
首先,简要介绍所使用的工具。 作为实现的示例,我将从原始报告中获取SerenityJS ,并以TypeScript作为测试脚本的语言。
我们将对TODO应用程序进行实验-TODO应用程序是用于创建简单任务列表的应用程序。 作为示例,我们将使用库的创建者Jan Molak的代码 (TypeScript代码,因此重点突出了一点)。
让我们从2009年开始
这时,Selenium WebDriver出现了,人们开始使用它。 不幸的是,在很多情况下,由于技术的新颖性和缺乏编程经验,这是错误的。 测试结果是完全复制粘贴,不受支持且“脆弱”的。
结果是负面的反馈和自动测试的不良声誉。 最后,Selenium WebDriver的创建者西蒙·斯图尔特(Simon Stewart)在缺席中回应了我的文章《 我的硒测试不稳定》! 。 普遍的信息是:“如果您的测试很脆弱并且不能很好地工作-这不是因为Selenium,而是因为测试本身不是很书面。”
要了解有什么问题,让我们看一下以下用户场景:
Feature: Add new items to the todo list In order to avoid having to remember things that need doing As a forgetful person I want to be able to record what I need to do in a place where I won't forget about them Scenario: Adding an item to a list with other items Given that James has a todo list containing Buy some cookies, Walk the dog When he adds Buy some cereal to his list Then his todo list should contain Buy some cookies, Walk the dog, Buy some cereal
天真的实现“在额头上”将如下所示:
import { browser, by, element, protractor } from 'protractor'; export = function todoUserSteps() { this.Given(/^.*that (.*) has a todo list containing (.*)$/, (name: string, items: string, callback: Function) => { browser.get('http://todomvc.com/examples/angularjs/'); browser.driver.manage().window().maximize(); listOf(items).forEach(item => { element(by.id('new-todo')).sendKeys(item, protractor.Key.ENTER); }); browser.driver.controlFlow().execute(callback); }); this.When(/^s?he adds (.*?) to (?:his|her) list$/, (itemName: string, callback: Function) => { element(by.id('new-todo')) .sendKeys(itemName, protractor.Key.ENTER) .then(callback); }); this.Then(/^.* todo list should contain (.*?)$/, (items: string, callback: Function) => { expect(element.all(by.repeater('todo in todos')).getText()) .to.eventually.eql(listOf(items)) .and.notify(callback); }); };
如您所见,此处使用了一个低级API,DOM的操作,css选择器的复制粘贴。 很明显,即使在UI上进行了很小的更改,您仍然必须在许多地方更改代码。
作为一种解决方案,Selenium必须提供足够好的解决方案来消除此类问题,同时让几乎没有面向对象编程经验的人们也可以使用Selenium。 这样的解决方案就是Page Object,一种组织代码的模式。
Martin Fowler 将其描述为HTML页面或其片段周围的抽象对象,使您无需触摸HTML本身即可与页面元素进行交互。 理想情况下,此类对象应允许客户端代码执行并查看人员可以执行并看到的所有内容。

按照此模式重写示例后,我们具有以下内容:
import { browser, by, element, protractor } from 'protractor'; class TodoList { What_Needs_To_Be_Done = element(by.id('new-todo')); Items = element.all(by.repeater('todo in todos')); addATodoItemCalled(itemName: string): PromiseLike<void> { return this.What_Needs_To_Be_Done.sendKeys(itemName, protractor.Key.ENTER); } displayedItems(): PromiseLike<string[]> { return this.Items.getText(); } } export = function todoUserSteps() { let todoList = new TodoList(); this.Given(/^.*that (.*) has a todo list containing (.*)$/, (name: string, items: string, callback: Function) => { browser.get('http://todomvc.com/examples/angularjs/'); browser.driver.manage().window().maximize(); listOf(items).forEach(item => { todoList.addATodoItemCalled(item); }); browser.driver.controlFlow().execute(callback); }); this.When(/^s?he adds (.*?) to (?:his|her) list$/, (itemName: string, callback: Function) => { todoList.addATodoItemCalled(itemName).then(() => callback()); }); this.Then(/^.* todo list should contain (.*?)$/, (items: string, callback: Function) => { expect(todoList.displayedItems()) .to.eventually.eql(listOf(items)) .and.notify(callback); }); };
乍看起来,它看起来不错-我们摆脱了重复并增加了封装。 但是有一个问题:在开发过程中,这类类经常会增长到巨大的规模。

您可以尝试将此代码分解为独立的组件,但是如果代码已达到这种状态,那么它已经紧密耦合在一起,以至于可能非常困难,并且将需要重写类本身以及使用此代码的整个客户端代码(测试用例)对象。 实际上,一大类不是疾病,而是一种症状( 代码气味 )。 主要问题是违反单一责任原则和开放封闭原则 。 Page Object隐含了页面的描述以及在一个实体中与页面进行交互的所有方式,如果不更改其代码就无法扩展页面。
对于我们的TODO类责任申请,如下所示( 文章图片):

如果您尝试根据SOLID原理分离此类,则会得到如下所示的信息:

领域模型
Page Object的下一个问题是它可以在页面上运行。 在任何验收测试中,通常都会使用用户案例。 行为驱动开发(BDD)和Gherkin语言非常适合此模型。 可以理解,用户达到目标(方案)的路径比特定的实现更为重要。 例如,如果您在所有测试中都使用了登录小部件,则在更改登录方法(表格已移动,切换为“单一登录”)时,您将必须更改所有测试。 在特别动态的项目中,这可能既耗时又耗时,并鼓励团队推迟编写测试,直到页面稳定为止(即使用例中一切都清楚了)。
为了解决这个问题,值得从另一侧来看。 我们介绍以下概念:
- 角色-都是谁的?
- 目标-他们(用户)为什么在这里?他们想实现什么目标?
- 任务-他们需要做什么才能实现这些目标?
- 动作-用户应该如何精确地与页面互动以完成任务?
因此,每个测试脚本实际上变成了针对用户的脚本 ,旨在执行特定的用户故事。 如果将这种方法与上述面向对象编程的原理相结合,则结果将是“ 编剧” (或“ 用户旅程” )模式。 他的想法和原则在2007年首次提出,甚至在PageObject之前就已经提出。
电影剧本
我们使用得到的原理重写代码。
詹姆斯是在这种情况下扮演我们用户的演员。 他知道如何使用浏览器。
let james = Actor.named('James').whoCan(BrowseTheWeb.using(protractor.browser));
James的目标是将第一项添加到他的列表中:
Scenario: Adding an item to a list with other items
为了实现此目标,James需要以下内容:
- 从包含多个项目的列表开始,
- 添加新项目。
我们可以将其分为两类:
import { PerformsTasks, Task } from 'serenity-js/lib/screenplay'; import { Open } from 'serenity-js/lib/screenplay-protractor'; import { AddATodoItem } from './add_a_todo_item'; export class Start implements Task { static withATodoListContaining(items: string[]) { return new Start(items); } performAs(actor: PerformsTasks) { return actor.attemptsTo( Open.browserOn('/examples/angularjs/'), ...this.addAll(this.items) ); } constructor(private items: string[]) { } private addAll(items: string[]): Task[] { return items.map(item => AddATodoItem.called(item)); } }
Task
接口需要定义performAs
方法,在执行过程中将actor转移到该方法。 attemptsTo
组合器函数,接受任意数量的混洗。 因此,您可以构建各种序列。 实际上,此任务所做的全部工作就是在所需页面上打开浏览器并在其中添加元素。 现在让我们看一下添加元素的任务:
import { PerformsTasks, Task } from 'serenity-js/lib/screenplay'; import { Enter } from 'serenity-js/lib/screenplay-protractor'; import { protractor } from 'protractor'; import { TodoList } from '../components/todo_list'; export class AddATodoItem implements Task { static called(itemName: string) { return new AddATodoItem(itemName); }
在这里更有趣,出现低级操作-在页面元素中输入文字,然后按Enter。 TodoList
是Page Object的“描述性”部分的全部内容,这里有css选择器:
import { Target, Question, Text } from 'serenity-js/lib/screenplay-protractor'; import { by } from 'protractor'; export class TodoList { static What_Needs_To_Be_Done = Target .the('"What needs to be done?" input box') .located(by.id('new-todo')); static Items = Target .the('List of Items') .located(by.repeater('todo in todos')); static Items_Displayed = Text.ofAll(TodoList.Items); }
好的,剩下的工作就是验证在所有操作之后是否显示了正确的信息。 SerenityJS提供了Question<T>
接口-一个返回显示值或值列表的实体(上面示例中的Text.ofAll
)。 一个人如何实现一个返回HTML元素文本的问题 :
export class Text implements Question<string> { public static of(target: Target): Text { return new Text(target); } answeredBy(actor: UsesAbilities): PromiseLike<string[]> { return BrowseTheWeb.as(actor).locate(this.target).getText(); } constructor(private target: Target) { } }
重要的是,浏览器绑定是可选的。 BrowseTheWeb
只是Ability ,它允许您与浏览器进行交互。 例如,您可以实现RecieveEmails
功能,该功能使演员可以读取字母(用于在网站上注册)。
综上所述 ,我们得到了这个方案(来自Jan Molak ):

和以下情况:
let actor: Actor; this.Given(/^.*that (.*) has a todo list containing (.*)$/, function (name: string, items: string) { actor = Actor.named(name).whoCan(BrowseTheWeb.using(protractor.browser)); return actor.attemptsTo( Start.withATodoListContaining(listOf(items)) ); }); this.When(/^s?he adds (.*?) to (?:his|her) list$/, function (itemName: string) { return actor.attemptsTo( AddATodoItem.called(itemName) ) }); this.Then(/^.* todo list should contain (.*?)$/, function (items: string) { return expect(actor.toSee(TodoList.Items_Displayed)).eventually.deep.equal(listOf(items)) });
最初,结果看起来有些庞大,但是随着系统的发展,重用代码的数量将增加,并且布局和页面更改将仅影响CSS选择器和低级任务/问题,而脚本和高级任务实际上保持不变。
实作
如果我们谈论的是图书馆,而不是小镇尝试应用这种模式,那么最受欢迎的是Java的Serenity BDD 。 Javascript / TypeScript使用其自己的SerenityJS端口。 开箱即用,她知道如何与Cucumber和Mocha一起工作。
快速查找还产生了一个用于.NET- tranquire的库。 因为我以前从未见过她,所以我无话可说。
使用Serenity和SerenityJS时,可以使用报告生成实用程序。
报告图片使用Mocha获取代码:
describe('Finding things to do', () => { describe('James can', () => { describe('remove filters so that the list', () => { it('shows all the items', () => Actor.named('James').attemptsTo( Start.withATodoListContaining([ 'Write some code', 'Walk the dog' ]), CompleteATodoItem.called('Write some code'), FilterItems.toShowOnly('Active'), FilterItems.toShowOnly('All'), Ensure.theListOnlyContains('Write some code', 'Walk the dog'), )); }); }); });
该报告将包含一般统计信息:

以及带有屏幕截图的步骤细分:

参考文献
一些相关文章:
- 重构页面对象:剧本/旅程模式的固体步骤
- 页面对象之外:具有宁静性和编剧模式的下一代测试自动化
- 宁静的BDD和剧本模式
论文:
库:
- 宁静BDD -Java
- 宁静JS -JavaScript / TypeScript
- 采购-.NET
如果您使用其他组织自动测试代码的方式/方式,请在评论中写下。