Thursday, February 20, 2014

Selenium and reusable scenarios

In my last post I've shown you how to structure your code describing the application under test so that it allows for easier construction of more readable integration tests. Today I'll try to complement that with the concept of reusable blocks of tests - scenarios.

Let me give you an example of a scenario that everyone can relate to: logging in to an application. Let's consider what does it actually take to perform this task:

  • navigation to the login page
  • entering proper credentials
  • clicking the "Login" button

Doing that in every test every time will soon make the tests unmaintainable. For example changing username and/or password or adding language to the login page when you do this 200 times is just going to be a huge pain in the neck.

Scenario concept to the rescue.

Let's imagine our base Page object contains this kind of method:

public <I extends Page, O extends Page> O run(Scenario<I, O> scenario) {
    return scenario.run(this);
}

Having defined this let's see the Scenario interface:

public interface Scenario<I extends Page, O extends Page> {
    O run(I entry);
}

The I generic parameter is the type of the page that you'd like to start the scenario on. The O generic parameter is the resulting page the scenario ends on.

Now the definition of the login scenario from the last post would look something like this (extended with language selector):

public class LoginScenario implements Scenario<LoginPage, HomePage> {
    private final String username;
    private final String password;
    private String language = "english";

    public LoginScenario(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public HomePage run(LoginPage entry) {
        return entry
            .assertHasProperUsernameLabel()
            .assertHasProperPasswordLabel()
            .enterCredentials(username, password)

            .selectLanguage(language)
            .clickLogin()
            .assertPageTitleIs("Welcome to this cool application!");
    }


    public LoginScenario language(String language) {
        this.language = language;
        return this;
    }
}

There are 2 types of parameters for this scenario: required (username and password) and optional with default values (language). The usage of fluent interface in setting up the scenario plays very nicely with the overall style of the integration tests. The fancy-looking generic Scenario interface allows to chain the execution of this reusable block of code in a fluent interface of the actual test:

public class LoginTest {
    @Test
    public void will_login_and_logout() {
        new LoginPage("http://localhost/myapp")
            .run(new LoginScenario("johndoe", "secret").language("german"))
            .logout();
    }
}

I hope this will help you keep your integration tests code DRY and maintainable!

Happy coding!