9月7日-8日,相约 2023 腾讯全球数字生态大会!聚焦产业未来发展新趋势! 了解详情
写点什么

超越页面对象:使用 Serenity 和 Screenplay 模式实现新一代的自动化测试

  • 2016-10-09
  • 本文字数:12371 字

    阅读完需:约 41 分钟

在如今快节奏的软件交付环境下,自动化验收测试是很有必要的。高质量的自动化验收测试能够减少手动测试和 bug 修复所耗费的时间,从而帮助我们更快地交付有价值的特性。将其与行为驱动开发(Behaviour-Driven Development)方式相结合的话,自动化验收测试还能指导和校验开发工作的开展,帮助团队聚焦于特性的构建,并确保这些特性是真正重要和可运行的。

但是自动化验收测试并不简单,与其他的软件开发活动一样,它是需要技巧、练习和纪律的。随着时间的推移,即便是具有最强意志力的团队也会发现他们的测试套件变得缓慢、脆弱和不可靠。在已有的套件上添加新的测试会变得越来越困难,团队会对自动化测试失去自信,减少在测试套件上的投入,进而影响团队的士气。我们甚至经常看到很有经验的团队,他们采用像页面对象(Page Object)这样的设计模式,依然会陷入到这种类型的困境之中。有些团队不熟悉高级程序员所使用的模式和设计原则(如 SOLID ),这样的话页面对象是一个很好的起点,但是在项目中,为团队成员尽早引入技术技能也是需要重点考虑的,从而能够避免这些挑战。

Screenplay 模式(之前被称为 Journey 模式)将 SOLID 设计原则应用到了自动化验收测试中,并帮助团队解决这些问题。它本质上就是采用 SOLID 设计原则对页面对象进行彻底重构所带来的结果,这个模式最早是由 Antony Marcano 在 2007 年至 2008 年所提出的,在 2009 年,Andy Palmer 按照他的理念对其进行了完善。直到 2013 年,Jan Molak 开始在这个领域开展工作,它才得到“Journey 模式”这个名字。尽管有很多人基于这个名称撰写了不少文章,但是本文的作者将其称之为 Screenplay 模式。

Screenplay 模式是一种编写高质量自动化验收测试的方法,它基于好的软件工程原则,比如单一职责原则(Single Responsibility Principle)开- 闭原则(Open-Closed Principle)。它坚持组合优于继承,并采用领域驱动设计(Domain Driven Design)中的理念来反映执行验收测试的领域,指导我们高效地使用抽象层。它鼓励好的测试习惯以及设计良好的测试套件,这些套件易于阅读、易于维护和扩展,这样的话,团队就能更加高效地编写更健壮更可靠的自动化测试。

Serenity BDD 是一个开源库,它的设计目的在于帮助我们编写更好、更有效的自动化验收测试,并借助这些验收测试生成高质量的测试报告和实时文档。在本文中我们将会看到,Serenity BDD 对 Screenplay 模式提供了内置的良好支持。

Screenplay 模式实战

在本文剩余的内容中,我们将会采用 Serenity BDD 来阐述 Screenplay 模式,不过这个模式本身在很大程度上是独立于语言和框架的。我们将要测试的就是著名的 TodoMVC 项目 AngularJS 实现(参见图 1)。

图 1 Todo 应用

简单起见,我们将会结合 JUnit 来使用 Serenity BDD,不过我们还可以结合 Cucumber-JVM JBehave 来使用 Serenity BDD,编写自动化验收测试的条件(criteria)。

现在,假设我们要实现“添加新的 Todo 条目”特性。按照“添加一个新的 Todo 条目”的描述,这个特性会有一个验收条件。如果我们手动测试这些场景的话,它可能会如下所示:

  • 添加一个新的 Todo 条目
    • 从一个空的 Todo 列表开始
    • 添加名为“Buy some milk”的条目
    • “Buy some milk”条目应该会显示在 Todo 列表中

Screenplay 模式一个大的卖点就是它能够按照业务的术语,借助易读的方法和对象 API 来表达验收测试的条件。例如,采用 Screenplay 模式,我们可以非常自然地自动化上述的场景,如下所示:

复制代码
givenThat(james).wasAbleTo(Start.withAnEmptyTodoList());
when(james).attemptsTo(AddATodoItem.called("Buy some milk"));
then(james).should(seeThat(TheItems.displayed(), hasItem("Buy some milk")));

如果你曾经使用过 Hamcrest 匹配器的话,那么这个模式对你来说将会非常熟悉。当我们使用 Hamcrest 匹配器的时候,会创建一个匹配器的实例,它会在 assertThat 方法中进行求值计算。类似的,AddATodoItem.called() 会返回“Task”的一个实例,这个实例会在稍后的 attemptsTo() 方法中进行求值操作。即便你可能不熟悉这些代码的内部是如何实现的,但是这个测试要阐述的内容和如何运行却是显而易见的。

稍后我们会看到编写这样的测试代码是非常容易的,这个过程和读代码的难度差不多。

声明式的编写方式能够让代码阅读起来类似于业务语言,这相对于命令式、关注于实现的方式更加易于维护,并且不易出错。如果代码阅读起来类似于业务规则的描述,那么业务逻辑中的错误将会很难进入到测试代码或应用程序本身的代码之中。

此外,Serenity 为这项测试所生成的测试报告也反映了这种叙述结构,在这个过程中采用的是业务术语,所以测试人员、业务分析师以及业务人员都能更容易地理解这些测试实际阐述的是什么(参见图 2)。

图 2:Serenity 的报告同时反映出了测试的意图和测试的实现

上面所列出的代码读起来非常整洁,但是你可能希望了解它在内部是如何实现的。现在,我们来看一下它是如何组合起来的。

Screenplay 模式的测试在运行方面与其他 Serenity 测试类似。

在撰写本文的时候,Serenity Screenplay 实现能够与 JUnit 和 Cucumber 进行集成。例如,在 JUnit 中,我们会用到 SerenityRunner JUnit runner,这与其他的 Serenity JUnit 测试是一样的。我们之前所看到的测试的完整代码如下所示,其中“Actor”担当了用户的角色,会与系统进行交互:

复制代码
@RunWith(SerenityRunner.class)
public class AddNewTodos {
Actor james = Actor.named("James");
@Managed private WebDriver hisBrowser;
@Before
public void jamesCanBrowseTheWeb() {
james.can(BrowseTheWeb.with(hisBrowser));
}
@Test
public void should_be_able_to_add_a_todo_item() {
givenThat(james).wasAbleTo(Start.withAnEmptyTodoList());
when(james).attemptsTo(AddATodoItem.called("Buy some milk"));
then(james).should(seeThat(TheItems.displayed(),
hasItem("Buy some milk")));
}
}

通过阅读代码,不难判断这个测试的意图是什么。但是,即便你之前使用过 Serenity,这里仍然还有一些我们所不熟悉的事情。在下面的章节中,我们将会近距离地看一下其中的细节。

Screenplay 模式鼓励采用严格的分层抽象

经验丰富的自动化测试人员会采用分层抽象的方式,将测试的意图(要试图实现什么目标)和实现的细节(如何实现)分离开来。通过将做什么和如何做进行分离,也就是分离意图与实现,分层抽象会让测试更加易于理解和维护。实际上,定义良好的分层抽象可能是编写高质量自动化测试的最重要因素。

在用户体验(User Experience,UX)设计中,我们会将用户与应用程序交互的方式拆分为 goal、task 和 action:

  • goal 使用业务术语描述了用户试图达到什么目的,也就是“为什么”要有这个场景。
  • task 在整体上描述了用户需要做些什么事情才能实现这一目标。
  • action 说明了用户要如何与系统进行交互才能完成一项特殊的任务,比如通过点击一个按钮或者在输入域中输入某个值。

我们将会看到,Screenplay 模式为 goal(场景标题)、task(场景中整体的抽象)和 action(最底层的抽象,比 task 的层级更低)提供了清晰的区分,这样的话,团队就能按照更加一致的方式编写分层的测试。

Screenplay 模式采用以 Actor 为中心的模型

测试描述了用户如何与应用程序进行交互以实现某个目标。鉴于此,如果测试能够以用户的视角来进行表述的话(而不是以“页面”的角度来进行表述),那么阅读起来会更加友好。

在 Screenplay 模式中,我们将与系统进行交互的用户称为 Actor。Actor 是 Screenplay 模式的核心(见图 3)。每个 Actor 有一项或多项 Ability,比如浏览 Web 或查询 RESTful Web 服务。Actor 也可以执行 Task,比如添加一个条目到 Todo 列表中。为了完成这些任务,它们需要与应用进行交互,比如在输入域中输入某个值或者点击一个按钮。我们将这种交互称为 Action。Actor 也可以提出 Question,询问应用的状态,比如读取屏幕上某个域的值或查询 Web 服务。

图 3:Screenplay 模式采用以 Actor 为中心的模式

在 Serenity 中,创建 Actor 非常简单,只需创建一个 Actor 类的实例并为其提供名称即可:

复制代码
Actor james = Actor.named("James");

我们发现为 Actor 设置一个真实的名称是非常有用的,而不应该使用一个通用的名称,比如“the user”。不同的名称可以作为不同用户角色或角色模型(Persona)的简写形式,从而使这些场景能够更容易地关联起来。关于使用角色模型的更多信息,可以参考 Jeff Patton 以“Pragmatic Personas”作为主题的演讲。

Actor 具有 Ability

Actor 具备做事情的能力,这样就能执行分派给它们的 task。所以,我们给 Actor 赋予“Ability”,如果采用更通俗的说法,这有点类似于超级英雄所具备的超能力。比如说,如果这是一个 Web 测试的话,我们需要 James 能够使用浏览器来访问 Web 内容。

Serenity BDD 能够与 Selenium WebDriver 很好地协作,并且可以非常便利地管理浏览器的生命周期。我们需要做的就是将 @Managed 注解用于 WebDriver 类型的变量上,如下所示:

复制代码
@Managed private WebDriver hisBrowser;

然后,我们可以让 James 按照如下的方式来使用这个浏览器:

复制代码
james.can(BrowseTheWeb.with(hisBrowser));

为了清晰地表明这是测试的前置条件(并且非常适于放到 JUnit 的 @Before 方法之中),我们可以使用语法糖方法 givenThat():

复制代码
givenThat(james).can(BrowseTheWeb.with(hisBrowser));

actor 的每项 ability 都会通过 Ability 类(在本例中,也就是 BrowseTheWeb)来进行表示,这个类能够跟踪 actor 为了达成该 ability 都需要哪些东西(例如,和浏览器进行交互的 WebDriver 实例)。将 actor 能做的事情(浏览 Web 内容、调用 Web 服务……)与 actor 本身进行分离,这会有助于扩展 actor 的 ability。例如,如果我们想要增加新的自定义 ability,只需要在测试类中添加一个新的 Ability 类即可。

Actor 执行 task

为了达成某个业务目标,actor 需要执行一定数量的 task。task 的一个典型样例就是“添加一项 Todo 条目”,它可以按照如下的方式来编写:

复制代码
james.attemptsTo(AddATodoItem.called("Buy some milk"))

或者,如果这项 task 是一个前置条件,而不是测试的主要内容,那么我们可以按照如下的方式编写:

复制代码
james.wasAbleTo(AddATodoItem.called("Buy some milk"))

我们接下来将其分解,看一下它是如何运行的。Screenplay 模式的核心就在于 actor 会执行一系列的 task。在 Serenity 中,这种机制是通过 Actor 类来实现的,它使用了命令模式(Command Pattern)的一种变体形式,在这里,actor 会执行每项 task,这是通过调用对应 Task 对象的一个名为performAs()方法来实现的(参见图 4):

图 4:actor 调用一系列 task 的 performAs() 方法

这里的 task 只是实现了 Task 接口的对象,它需要实现performAs(actor)方法。实际上,你可以将任意的 Task 类看做只有一个基本performAs()方法和一个辅助方法的类。

Task 可以通过注解和构造者模式创建

为了达到所宣称的魔力,Serenity BDD 需要对测试过程中所用到的 task 和 action 对象进行 instrument 操作。最简单的方式就是由 Serenity 负责创建它,就像其他的 Serenity step 库一样,这需要用到 @Steps 注解。在下面的代码片段中,Serenity 将会我们创建openTheApplication域,这样的话,James 就可以使用它来打开应用了:

复制代码
@Steps private OpenTheApplication openTheApplication;
james.attemptsTo(openTheApplication);

对于非常简单的 task 或 action 来说,这是可行的,比如构造过程不需要参数的 task 或 action。但是对于更加复杂的 task 或 action 来说,工厂或构造者模式(就像我们之前所用到的 AddATodoItem)会更加便利。经验丰富的人往往会让构造者方法和类名读起来就像是一个英文句子,这样的话,task 的意图(intent)会非常清晰:

复制代码
AddATodoItem.called("Buy some milk")

Serenity BDD 提供了专门的 Instrumented 类,借助它能够非常便利地使用构建者模式创建 task 或 action。例如,AddATodoItem类有一个不可变的域,名为thingToDo,这个域中包含了新 Todo 条目的文本。

复制代码
public class AddATodoItem implements Task {
private final String thingToDo;
protected AddATodoItem(String thingToDo) { this.thingToDo = thingToDo; }
}

我们可以通过Instrumented.instanceOf().withProperties()方法来调用这个构造器,如下所示:

复制代码
public class AddATodoItem implements Task {
private final String thingToDo;
protected AddATodoItem(String thingToDo) { this.thingToDo = thingToDo; }
public static AddATodoItem called(String thingToDo) {
return Instrumented.instanceOf(AddATodoItem.class).
withProperties(thingToDo);
}
}

高层次的 task 由其他低层次 task 或 action 组合而成

为了完成任务,高层次的 task 通常会调用低层次的业务 task 或 action,它们会更加直接地与应用进行交互。在实践中,这意味着某个 task 的performAs()方法一般会调用其他低层次的 task 或者以其他的方式与应用进行交互。例如,添加一个 todo 条目需要两个 UI 操作:

  1. 将 todo 的文字输入到文本域中
  2. 点击 Return 键

在前面我们所使用的AddATodoItem类中,performAs()方法就是这样做的:

复制代码
private final String thingToDo;
@Step("{0} adds a todo item called #thingToDo")
public void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(thingToDo)
.into(NewTodoForm.NEW_TODO_FIELD)
.thenHit(RETURN)
);
}

在实际的实现中,我们用到了 Enter 类,这是 Serenity 自带的预定义 Action。Action 类与 Task 类非常相似,不过它们更加关注与应用的直接交互。Serenity 提供了一组基础的 Action 类,用于核心的 UI 交互,比如为输入域赋值、点击元素或者从下拉列表中选择值。在实践中,它们提供了一个便利和易读的 DSL,借此能够描述执行 task 所需的低层次 UI 交互。

在 Serenity Screenplay 的实现中,我们会使用一个特殊的 Target 类来识别元素,它会借助 CSS(默认)或 XPATH 来进行识别。Target 对象会关联一个 WebDriver 选择器,这个过程会使用一个易于人类阅读的标注,这个标注将会显示到测试报告中,这样的话,报告会更易读。Target 对象的定义如下所示:

复制代码
Target WHAT_NEEDS_TO_BE_DONE = Target.the(
"'What needs to be done?' field").locatedBy("#new-todo")
;

Target 通常会存储在很小的页面对象中,这些类只会负责一件事情,也就是如何为特定的 UI 组件定位元素,比如下面所示的ToDoList类:

复制代码
public class ToDoList {
public static Target WHAT_NEEDS_TO_BE_DONE = Target.the(
"'What needs to be done?' field").locatedBy("#new-todo");
public static Target ITEMS = Target.the(
"List of todo items").locatedBy(".view label");
public static Target ITEMS_LEFT = Target.the(
"Count of items left").locatedBy("#todo-count strong");
public static Target TOGGLE_ALL = Target.the(
"Toggle all items link").locatedBy("#toggle-all");
public static Target CLEAR_COMPLETED = Target.the(
"Clear completed link").locatedBy("#clear-completed");
public static Target FILTER = Target.the(
"filter").locatedBy("//*[@id='filters']//a[.='{0}']");
public static Target SELECTED_FILTER = Target.the(
"selected filter").locatedBy("#filters li .selected");
}

performAs()方法上,@Step注解所提供的信息将会决定这个 task 在测试报告中会如何显示:

复制代码
@Step("{0} adds a todo item called #thingToDo")
public void performAs(T actor) {…}

在 @Step 注解中,可以通过 hash(“#”)前缀引用任意的成员变量(比如样例中的“#thingToDo”)。我们还可以通过特殊的“{0}”占位符来引用 actor 本身。最终所形成的结果就是每项业务 task 如何执行的详尽描述(参加图 5)。

图 5:测试报告展现了每项 task 和 UI 交互的细节

task 可以作为构建块供其他 task 使用

在其他更高层次的 task 中,我们可以很容易地对 task 进行重用。例如,示例项目使用AddTodoItems task 将一些todo 条目添加到了列表中,如下所示:

复制代码
givenThat(james).wasAbleTo(AddTodoItems.called("Walk the dog",
"Put out the garbage"));

这个 task 的定义使用了AddATodoItem类,如下所示:

复制代码
public class AddTodoItems implements Task {
private final List todos;
protected AddTodoItems(List items) {
this.todos = ImmutableList.copyOf(items); }
@Step("{0} adds the todo items called #todos")
public void performAs(T actor) {
todos.forEach(
todo -> actor.attemptsTo(
AddATodoItem.called(todo)
)
);
}
public static AddTodoItems called(String... items) {
return Instrumented.instanceOf(AddTodoItems.class).
withProperties(asList(items));
}
}

按照这种方式,重用已有的 task 来构建更为复杂的业务 task 是非常常见的。我们发现了一个有用的约定就是打破 Java 通用的惯例,将静态的创建方法放在performAs() 方法下面。这是因为在一个 Task 中,最有价值的信息是它是如何执行的,而不是它是如何创建出来的。

Actor 可以针对应用的状态提出 question

一个典型的自动化验收测试会包含三部分:

  1. 准备一些测试数据和 / 或让应用进入到一个已知的状态
  2. 执行一些 action
  3. 将应用的新状态与预期进行对比。

从测试的角度来看,第三步是真正的价值所在——在这一步中会检验应用是否按照预期的方式来运行。

在传统的 Serenity 测试中,我们会使用 Hamcrest 或 AssertJ 这样的库来编写一个断言,检查输出与预期值是否相符。如果采用 Serenity Screenplay 实现的话,我们表达断言的方式会使用一个灵活、流畅的 API,它与我们编写 Task 和 Action 时非常类似。在上面的测试中,断言如下所示:

复制代码
then(james).should(seeThat(TheItems.displayed(), hasItem("Buy some milk")));

这个代码的结构如图 6 所示。

图 6:Serenity Screenplay 断言

如你所料,这个代码会检查从应用中获取到的值(屏幕上展现的条目)与一个预期值(Hamcrest 表达式所描述的)是否相符。但是,我们这里并没有传递实际值,而是传入了一个 Question 对象。Question 对象的角色是回答关于应用准确状态的问题,这个问题会从 actor 的视角来进行回答,通常还会使用 actor 的 ability 来完成这一点。

在测试报告中,Question 会以人类易读的方式来进行渲染

关于 Screenplay 断言,另外一件很棒的事情就是在测试报告中,它们会以非常易读的方式展现,这样的话测试的意图更加清晰,错误的诊断也会更加容易。(参见图 8)。

图 8:在测试报告中,Question 会以人类易读的方式来进行渲染

Actor 使用它们的 ability 来与系统进行交互

让我们在另外一个测试中,实际看一下这个原则。Todo 应用在底部的左侧有一个计数器,它展现了剩余条目的数量(见图 7)。

图 7:在列表的左下角,展现了剩余条目的数量

描述和验证这种行为的测试如下所示:

复制代码
@Test
public void should_see_the_number_of_todos_decrease_when_an_item_is_completed()
{
givenThat(james).wasAbleTo(Start.withATodoListContaining(
"Walk the dog", "Put out the garbage"));
when(james).attemptsTo(
CompleteItem.called("Walk the dog")
);
then(james).should(seeThat(TheItems.leftCount(), is(1)));
}

测试需要检查剩余条目的数量(通过”items left“计数器来表示)为 1。测试中最后一行的断言如下所示:

复制代码
then(james).should(seeThat(TheItems.leftCount(), is(1)));

静态的TheItems.leftCount()方法是一个简单的工厂方法,它会返回ItemsLeftCounter类的一个新实例,如下所示:

复制代码
public class TheItems {
public static Question> displayed() {
return new DisplayedItems();
}
public static Question leftCount() {
return new ItemsLeftCounter();
}
}

这样的话,能够让代码阅读起来非常流畅。

Question 对象是通过 ItemsLeftCounter来类定义的。这个类有一个明确的责任:读取 todo 列表底部的文本中剩余条目的数量。

Question 对象与 Task、Action 对象类似。但是,与 Task 和 Action 所使用的 performAs()方法不同,Question 类需要实现 answeredBy(actor)方法,并返回特定类型的结果。在这里,ItemsLeftCounter 被配置为返回 Integer。

复制代码
public class ItemsLeftCounter implements Question {
@Override
public Integer answeredBy(Actor actor) {
return Text.of(TodoCounter.ITEM_COUNT)
.viewedBy(actor)
.asInteger();
}
}

Serenity Screenplay 提供了多个低层级的 UI 交互类,通过它们,我们能够以声明式的方式来查询 Web 页面。在上面的代码中,answeredBy()使用了Text交互类,以此来获取剩余条目数量的文本,并将其转换为一个 integer。

如前面所示,用于定位元素的逻辑重构到了TodoList类中:

复制代码
public static Target ITEMS_LEFT = Target.the("Count of items left").
locatedBy("#todo-count strong");

再次强调,这个代码会在三个层级执行,每个层级都有其特有的责任:

  • 顶级的步骤会对应用的状态进行断言: then(james).should(seeThat(TheItems.leftCount(), is(1)));
  • ItemsLeftCounter Question 类查询应用的状态,并且按照断言预期的格式返回结果;
  • TodoList 类会存储 Question 类所需的 Web 元素的位置。

编写自定义的 UI 交互

Serenity Screenplay 自带了一系列低层级的 UI 交互类,很少会出现这些类无法满足需求的场景。在本例中,可以直接使用 WebDriver API 进行交互,我们通过编写自定义的 Action 类来展现这种方式,这其实很容易。

例如,假设我们希望删除 todo 列表中的一个条目,可以使用如下的代码行:

复制代码
when(james).attemptsTo(
DeleteAnItem.called("Walk the dog")
);

现在,就我们的应用实现来说,Delete 按钮并没有接受常规的WebDriver点击,我们需要直接调用 JavaScript 事件。在样例代码中,能够看到完整的类,在DeleteAnItem task 的performAs()方法中使用了一个自定义的 Action 类,名为JSClick,这个类会触发 JavaScript 事件:

复制代码
@Step("{0} deletes the item '#itemName'")
public void performAs(T theActor) {
Target deleteButton = TodoListItem.DELETE_ITEM_BUTTON.of(itemName);
theActor.attemptsTo(JSClick.on(deleteButton));
}

JSClick类是 Action 接口的简单实现,如下所示:

复制代码
public class JSClick implements Action {
private final Target target;
@Override
@Step("{0} clicks on #target")
public void performAs(T theActor) {
WebElement targetElement = target.resolveFor(theActor);
BrowseTheWeb.as(theActor).evaluateJavascript(
"arguments[0].click()", targetElement);
}
public static Action on(Target target) {
return instrumented(JSClick.class, target);
}
public JSClick(Target target) {
this.target = target;
}
}

这里的重点代码在 performAs() 方法中,我们使用BrowseTheWeb类来访问 actor 的Ability,以实现对浏览器的使用。这样的话,就完全可以访问Serenity WebDriver API了:

复制代码
BrowseTheWeb.as(theActor).
evaluateJavascript("arguments[0].click()", targetElement);

(这是一个比较牵强的例子,因为 Serenity 已经提供了一个交互类,借助这个类也能够将 Javascript 注入到页面中)

页面对象变得更小并且更具体

Screenplay 模式所带来的一个很有意思的后果就是它会改变我们使用和思考页面对象的方式。页面对象的理念在于封装 UI 相关的逻辑,将访问或查询 Web 页面以及 Web 页面上的元素封装到一个更为业务友好的 API 中。就理念本身而言,这是很好的。

但是页面对象(以及传统的 Serenity step 库)的问题在于很难将它们组织好。随着测试套件的增长,它们的量也会不断增长,将会变得更大且更加难以维护。这其实也没有什么可奇怪的,因为这样的页面对象同时违背了单一职责原则(Single Responsibility Principle,SRP)和开 - 闭原则(Open-Closed Principle,OCP)——也就是 SOLID 中所指的“S”和“O”。在很多测试套件中,页面对象最终会具有复杂的层级结构,这些对象会从父页面对象中继承一些“通用”的行为,比如菜单栏或注销按钮,这违背了组合优于继承的原则。新的测试一般都会需要修改已有的页面对象类,这样的话,就有引入 bug 的风险。

当我们使用 Screenplay 模式的时候,页面对象会变得更小更专注,针对屏幕上的特定组件,它们会具有一个非常明确的指令来定位元素。在编写完之后,除非底层的 Web 界面发生变化,否则的话,它们都会保持不变。

BDD 风格的场景并不是强制性的

有些人习惯在 xUnit 框架中编写验收测试,他们可能并不喜欢 Given/When/Then 这种编写场景(scenario)的风格。这些方法纯粹是为了易读性,它们有助于更加明确地表达我们的意图,也就是表示事先安排(given)、行为(when)以及断言(then)。并不是每个人都喜欢这种风格,所以我们也不强制这样做,你可以采用如下所示的替代方式:

复制代码
james.wasAbleTo(Start.withAnEmptyTodoList());
james.attemptsTo(AddATodoItem.called("Buy some milk"));
james.should(seeThat(toDoItems, hasItem("Buy some milk")));

在上面的代码中,用户的意图隐含在“wasAbleTo”、“attemptsTo”和“should”方法中,但是,我们相信将意图明确地表示出来会对我们和日后阅读代码的人都有好处,所以推荐使用内置的 givenThat()、when() 和 then() 方法。如果你在 Cucumber 中采取这种方式的话,那么可以不用再去考虑 Given/When/Then 方法,因为在 Cucumber step 的定义中,意图通常是非常明确的。

结论

Screenplay 模式是一种编写自动化验收测试的方式,它建立在良好的软件工程原则之上,使我们能够更容易地编写整洁、易读、可扩展和高可维护性的测试代码。采用这种方式的一个结果就是页面对象模式可能会被彻底重构,转向了 SOLID 原则。在 Serenity BDD 中,对 Screenplay 模式的支持会带来很多令人兴奋的可能性。尤其是:

  • Screenplay 模式鼓励声明式的编写风格,这样的话,编写易于理解和维护的代码会更加简单;
  • 相对于传统的 Serenity step 方法,Task、Action 和 Question 类更加灵活、可重用和易读;
  • 将 actor 的 ability 进行分离会带来很大的灵活性。例如,我们可以很容易地编写多个 actor 使用不同浏览器实例的测试代码。

与很多好的软件开发实践类似,Screenplay 模式起初会需要一些训练。有些人会认为首先需要设计一个可读的、类似于 DSL 的 API,这个 API 由组织良好的 task、action 和 question 所组成。随着测试套件的增长,所带来的收益很快就会非常明显,由可重用组件所组成的库有助于加快测试编写的过程,使其达到一个可持续的增长率,从而减少以往持续维护自动化测试套件所引起的摩擦。

延伸阅读

本文只是 Screenplay 模式及其 Serenity 实现的一个简介。要学习更多知识的最好方式就是研究可运行的代码,你可以在 Github 上找到该示例项目的源码。

参考文献

关于作者

John Ferguson Smart是一位经验丰富的作者、演说家和教练,专注于敏捷交付实践,目前他在伦敦居住。在敏捷社区,他发表过很多文章和演讲,因此是国际知名的演讲者,尤其是在 BDD、TDD、测试自动化、软件匠艺以及团队协作领域。John 通过更高效的协作交流技术以及更好的技术实践,帮助世界范围内的很多组织和团队更快地交付更棒的软件。 LinkedIn Github Web 站点

Antony Marcano在社区非常知名,这要归因于他在 BDD、用户故事、测试以及在 Ruby 和 Java 中编写 fluent API & DSL 等方面的思想。在敏捷项目以及各种规模的项目改造方面,他有着 16 年以上的经验,大多数的时间他都是一个实践者,因为他会担任教练。他会通过各种方式分享他的经验,包括参与图书的编写,例如《Agile Coaching》和《Agile Testing》,在《Bridging the Communication Gap》和《Software Craftsmanship Apprenticeship Patterns》等图书中,也曾引用过他的经验。在国际会议上,他会持续做敏捷开发相关的演讲,同时还会在牛津大学做定期的客座演讲。

Andy Palmer是开创性的截屏录制站点 PairWith.us 的共同创始人,他在国际会议上经常发表演讲。Andy 为无数组织缩短了项目交付周期,这要归功于他解决复杂技术问题以及能够抓住问题本质的特长。依靠这个领域的经验,Andy 能够将大型项目的交付周期缩短一半。他有着 15 年以上的经验,担任过的重要角色包括团队和管理教练、开发人员以及系统管理员,在 DevOps 这个术语出现之前,他就依靠自身的经验弥合了沟通方面的鸿沟。

Jan Molak是一个全栈开发人员和教练,他过去的 12 年间,构建和交付了各种类型的软件,从获奖的 AAA 视频游戏、通过 Web 站点和 webapps 实现的 MMO RPG,再到搜索引擎、复杂的事件处理和金融系统。Jan 主要的关注点在于,通过高效的工程实践,帮助组织更快更可靠地交付有价值、高质量的软件。Jan 是开源项目的活跃贡献者,他是 Jenkins Build Monitor 的作者,这个工具帮助世界范围内成千上万的公司保证了构建的正确性,确保交付过程能够顺利执行。

查看英文原文: Beyond Page Objects: Next Generation Test Automation with Serenity and the Screenplay Pattern

活动推荐:

2023年9月3-5日,「QCon全球软件开发大会·北京站」 将在北京•富力万丽酒店举办。此次大会以「启航·AIGC软件工程变革」为主题,策划了大前端融合提效、大模型应用落地、面向 AI 的存储、AIGC 浪潮下的研发效能提升、LLMOps、异构算力、微服务架构治理、业务安全技术、构建未来软件的编程语言、FinOps 等近30个精彩专题。咨询购票可联系票务经理 18514549229(微信同手机号)。

2016-10-09 18:163602

评论

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

Python超实用!批量重命名文件/文件夹,只需1行代码

程序员晚枫

Python 文件管理 自动化办公

端口转发工具Rinetd详细入门教程

百度搜索:蓝易云

云计算 Linux 运维 端口 Rinetd

微信朋友圈的高性能复杂度架构

艾瑾行

#架构训练营

开放原子开源基金会TOC(技术监督委员会)第七十七次全体会议

开放原子开源基金会

覆巢之下(1)

于哲

拜托,别在agent中依赖fastjson了

夏奇

Java Agent 类加载 架构设计 Fastjson

PoseiSwap:首个基于模块化设施构建的订单簿 DEX

西柚子

Centos7安装配置Hive教程。

百度搜索:蓝易云

云计算 hive Linux centos 运维

linux nfs共享存储服务详细解释。

百度搜索:蓝易云

云计算 Linux 运维 云服务器 NFS

医疗知识图谱问答 —— 数据同步

北桥苏

Python neo4j 知识图谱

一致性哈希算法

java易二三

程序员 算法 计算机 科技

Sprint Boot学习路线3

小万哥

Java spring 后端 springboot SpringCloud

PoseiSwap:首个基于模块化设施构建的订单簿 DEX

石头财经

git remote 命令详解

百度搜索:蓝易云

git 云计算 Linux 运维 Remote

深入浅出DAX:数据分析

TiAmo

数据分析 数据处理 DAX

2023年度姑苏创新创业领军人才计划项目指南来了!

科兴未来News

MTS性能监控你知道多少

GreatSQL

greatsql mts

2023年7月文章一览

codists

编程人生

敏捷产品路线图管理实例,产品路线图工具

顿顿顿

Scrum 敏捷开发管理 产品路线图工具

大模型真的会“好事多模”吗?

脑极体

大模型

文心一言 VS 讯飞星火 VS chatgpt (69)-- 算法导论6.5 8题

福大大架构师每日一题

福大大架构师每日一题

PoseiSwap:首个基于模块化设施构建的订单簿 DEX

BlockChain先知

千云探探监测到7月25日法国巴黎Facebook网络恢复正常

郑州埃文科技

网络性能

Gartner发布《2023年中国ICT技术成熟度曲线》,明道云连续两年入选样本厂商

明道云

javascript函数基础

timerring

JavaScript

活动回顾|OpenTiny:跨框架前端组件库的技术实现和实践(内含ppt课件)

OpenTiny社区

开源 前端 UI组件库

  • 扫码添加小助手
    领取最新资料包
超越页面对象:使用Serenity和Screenplay模式实现新一代的自动化测试_Java_Antony Marcano_InfoQ精选文章