[UI自动化设计模式]超越 Page Objects:使用Serenity和ScreenplayPattern新一代的自动化测试

402 阅读27分钟
原文链接: mp.weixin.qq.com

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

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

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

    Serenity BDD是一个开源库,它的设计目的在于帮助我们编写更好、更有效的自动化验收测试,并借助这些验收测试生成高质量的测试报告和实时文档。在本文中我们将会看到,Serenity BDD对Screenplay模式提供了内置的良好支持。Screenplay模式是一种编写高质量自动化验收测试的方法,它基于好的软件工程原则,比如单一职责原则(Single Responsibility Principle)和开-闭原则(Open-Closed Principle)。它坚持组合优于继承,并采用领域驱动设计(Domain Driven Design)中的理念来反映执行验收测试的领域,指导我们高效地使用抽象层。它鼓励好的测试习惯以及设计良好的测试套件,这些套件易于阅读、易于维护和扩展,这样的话,团队就能更加高效地编写更健壮更可靠的自动化测试。

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进行重用。例如,示例项目使用AddTodoItemstask将一些 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上找到该示例项目的源码。

参考文献

  • Kevin Matz所编写的Designing Usable Apps: An agile approach to User Experience Design

  • “A BIT OF UCD FOR BDD & ATDD: GOALS -> TASKS -> ACTIONS” – Antony Marcano, 

  • “A journey beyond the page object pattern” - Antony Marcano, Jan Molak, Kostas Mamalis 

  • JNarrate: The original reference implementation of the Screenplay Pattern

  • 命令模式:《设计模式:可复用面向对象软件的基础》 - Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides

  • TodoMVC:本文中所测试的Todo应用。

关于作者

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

本文转自:http://www.infoq.com/cn/articles/Beyond-Page-Objects-Test-Automation-Serenity-Screenplay