Screenplay - not a Page Object

    Over time, it becomes more difficult to make changes to any product, and the risk of not only releasing new features, but also breaking old ones, increases. Often, instead of manually checking the entire project, they try to automate this process. If you talk with people who test interface, walk through conferences, it becomes clear that in the world of web testing Selenium rules, and the vast majority of people use Page Object as code organization.


    But for me, as a programmer, for some reason I never liked this pattern and the code that I saw with different teams - the letters SOLID sounded in my head. But I was ready to accept the fact that testers write code as they like, due to the lack of alternatives, like about a year ago, on Angular Connect, I heard a report on testing Angular applications using the Screenplay pattern. Now I want to share.



    Guinea pig


    First, a brief description of the tools used. As an example of implementation, I will take SerenityJS from the original report, with TypeScript as the language of the test scripts.


    We will set up experiments above the TODO application - an application for creating a simple task list. The examples will be the code from Jan Molak , the creator of the library (TypeScript code, so the backlight went a little).


    Let's start in 2009


    At this time, Selenium WebDriver appeared, and people began to use it. Unfortunately, in a large number of cases, due to the novelty of technology and the lack of programming experience, it is wrong. Tests were obtained full copy-paste, unsupported and "fragile".


    The result is negative feedback and a bad reputation at autotests. As a result, Simon Stewart, the creator of Selenium WebDriver, responded in absentia to this in his article, My Selenium Tests Aren't Stable! . The general message is: “if your tests are fragile and do not work well, this is not because of Selenium, but because the tests themselves are not very well written”.


    To understand what is going on, let's look at the following custom script:


    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
    

    A naive implementation "in the forehead" will look like this:


    import { browser, by, element, protractor } from'protractor';
    export = functiontodoUserSteps() {
        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);
        });
    };

    As you can see, here we use low-level API, manipulations with DOM, copy-paste css-selectors. It is clear that in the future even with a small change in the UI will have to change the code in many places.


    As a solution, Selenium had to offer something good enough to get rid of such problems, and at the same time it was accessible to people with little or no experience in object-oriented programming. This decision was the Page Object - a pattern for organizing code.


    Martin Fowler describes it as an abstraction object around an HTML page or a fragment of it, allowing you to interact with the elements of the page without touching the HTML itself. Ideally, such an object should allow client code to do and see everything that a person can do and see.


    By rewriting our example in accordance with this pattern, we have the following:


    import { browser, by, element, protractor } from'protractor';
    classTodoList{
        What_Needs_To_Be_Done = element(by.id('new-todo'));
        Items = element.all(by.repeater('todo in todos'));
        addATodoItemCalled(itemName: string): PromiseLike<void> {
            returnthis.What_Needs_To_Be_Done.sendKeys(itemName, protractor.Key.ENTER);
        }
        displayedItems(): PromiseLike<string[]> {
            returnthis.Items.getText();
        }
    }
    export = functiontodoUserSteps() {
        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);
        });
    };

    At first glance, it looks good - we got rid of duplication and added encapsulations. But there is one problem: very often such classes in the development process grow to enormous sizes.



    You can try to decompose this code into independent components, but if the code has reached such a state, it is already so strongly connected that it can be very difficult and will require rewriting both the class itself and all client code (test cases) that use this an object. In fact, a huge class is not a disease, but a symptom ( code smell ). The main problem is a violation of the Single Responsibility Principle and the Open Closed Principle . Page Object implies the description of the page and all the ways to interact with it in one entity that cannot be expanded without changing its code.


    For our TODO, class responsibility applications are as follows (images from the article ):
    Responsibility


    If you try to divide this class based on the principles of SOLID, you get something like the following:


    Domain model


    The next problem we have with Page Object is that it handles pages. In any acceptance testing, the use of user scenarios is common. Behavior Driven Development (BDD) and the Gherkin language fit perfectly on this model. It is understood that the user's path to the goal (scenario) is more important than the specific implementation. For example, if you have a login widget in all tests, if you change the login method (the form has moved, you switched to Single Sign On) you will have to change all the tests. In particularly dynamic projects, this can be costly and time consuming and make the team push the writing of tests until the pages stabilize (even if everything is clear with the use cases).


    To solve this problem, it is worth looking at it from the other side. We introduce the following concepts:


    1. Roles - for whom is all this?
    2. Goals — why are they (users) here and what do they want to achieve?
    3. Tasks - what do they need to do to achieve these goals?
    4. Actions - how exactly does the user have to interact with the page to complete the task?

    Thus, each test scenario becomes, in fact, a scenario for the user, aimed at the execution of a particular user story. If we combine this approach with the principles of object-oriented programming described above, the result will be the Screenplay (or User Journey ) pattern. His ideas and principles were first voiced in 2007, even before PageObject.


    Screenplay


    Rewrite our code using the resulting principles.


    James is the actor who will play our user in this scenario. He knows how to use the browser.


    let james = Actor.named('James').whoCan(BrowseTheWeb.using(protractor.browser));

    James’s goal is to add the first item to his list:


     Scenario: Adding an item to a list with other items

    To realize this goal, James needs the following:


    1. start with a list containing several items
    2. add new item.

    We can break it into two classes:


    import { PerformsTasks, Task } from'serenity-js/lib/screenplay';
    import { Open } from'serenity-js/lib/screenplay-protractor';
    import { AddATodoItem } from'./add_a_todo_item';
    exportclassStartimplementsTask{
        static withATodoListContaining(items: string[]) {
            returnnew 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));
        }
    }

    The interface Taskrequires defining a method performAsto which the actor will be transferred during execution. attemptsTo- combinator function, accepts any number of tasks. This way you can build a variety of sequences. In fact, everything that this task does, it opens the browser on the necessary page and adds elements there. Now let's look at the add item taska:


    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';
    exportclassAddATodoItemimplementsTask{
        static called(itemName: string) {
            returnnew AddATodoItem(itemName);
        }
        // required by the Task interface
        performAs(actor: PerformsTasks): PromiseLike<void> {
            // delegates the work to lower-level tasksreturn actor.attemptsTo(
                Enter.theValue(this.itemName)
                    .into(TodoList.What_Needs_To_Be_Done)
                    .thenHit(protractor.Key.ENTER)
            );
        }
        constructor(private itemName: string) 
        }
    }

    Here it is more interesting, low-level actions appear - enter text into the page element and press Enter. TodoList- this is all that remains of the “descriptive” part of Page Object, here we have css-selectors:


    import { Target, Question, Text } from'serenity-js/lib/screenplay-protractor';
    import { by } from'protractor';
    exportclassTodoList{
        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);
    }

    Ok, it remains to check that after all the manipulations the correct information is displayed. SerenityJS offers an interface Question<T>- an entity that returns a display value or a list of values ​​( Text.ofAllin the example above). How could you implement such a question , returning the text of the HTML element:


    exportclassTextimplementsQuestion<string> {
        public staticof(target: Target): Text {
            returnnew Text(target);
        }
        answeredBy(actor: UsesAbilities): PromiseLike<string[]> {
            return BrowseTheWeb.as(actor).locate(this.target).getText();
        }
        constructor(private target: Target) {
        }
    }

    What is important, binding to the browser is optional. BrowseTheWebit's just Ability , which allows you to interact with the browser. You can implement, for example, RecieveEmailsability, which will allow the actor to read letters (to register on the site).


    Putting it all together, we get the following scheme (from Jan Molak ):


    and the following script:


    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))
    });

    Initially, the resulting result looks somewhat massive, but as the system grows, the amount of reused code will increase, and changes in layout and pages will only affect css selectors and low-level tasks / questions, leaving the scripts and high-level tasks themselves almost unchanged.


    Implementations


    If we talk about libraries, and not about shtetl attempts to use this pattern, the most popular is Serenity BDD for Java. Under JavaScript / TypeScript its port SerenityJS is used . Out of the box, she knows how to work with Cucumber and Mocha.


    A quick search also issued a library for .NET - tranquire . I can not say anything about it, because I have not met before.


    When using Serenity and SerenityJS, you can use the report generation utility .


    Report with pictures

    Take the code using 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'),
                ));
            });
        });
    });

    The report will contain as general statistics:


    and breakdown by steps with screenshots:


    Links


    Several related articles:


    1. Page Objects Refactored: SOLID Steps to the Screenplay / Journey Pattern
    2. Beyond Page Objects: Screen Generation and Serenity Screenplay Pattern
    3. Serenity BDD and the Screenplay Pattern

    Reports:





    Libraries:


    1. Serenity BDD - Java
    2. Serenity JS - JavaScript / TypeScript
    3. tranquire - .NET

    Please write in the comments if you use other patterns / ways to organize the autotest code.


    Also popular now: