Separation of concern when using Selenium

Filed under: Java, Selenium, Test automation, — Tags: Page Object Pattern, WebDriver — Thomas Sundberg — 2016-01-14

A lot of people want to automate testing of their web applications. This is definitely a good thing. But it happens that they focus more on the tooling than the testing.

Most testing in a web application should not be done through the user interface. Instead, most of the testing of the domain model can, and should, be tested using unit testing.

An example of something that could be tested using unit tests is the password validation algorithm. Passwords are entered in the user interface of the application. The logic that validates that it is a proper password should probably be a unit test. There will be code that receives a String or similar and return true or false.

Some functionality should, however, be tested through the user interface. In these cases, it is valuable to separate responsibilities. Selenium is not a tool for verification, but rather for navigation using an actual browser.

Verification should be done using other tools. They include unit testing frameworks or BDD frameworks. Being a Java developer, I often use JUnit. In some cases I would probably use Cucumber. It depends on the audience and their ability to read code.

Separating navigation from verification is one way to separate the concern. It leads to a pattern known as the Page Object Pattern. There are different reasons to why the navigation changes and why the verification changes. This means using that page objects makes it easier to adhere to the Single Responsibility Principle, SRP.

Using page objects saves a lot of problems when the layout, but not the logic, is changed in a web application.

What is a page object then? It is a class that abstracts away interaction with a web page. An example could be entering values in a form and submitting it. The methods in the page object knows the name of different widgets so the user can work on a higher abstraction level. Instead of working on the level send keys to web element foo, the user can say "buy 4 Star Wars Lego boxes" and not care about how the widget that is used is located.

Instead of mixing verification code and navigation code, the test writer is able to focus on the expected behavior and nothing else. Let me show you a small example using the test application I use in my Selenium course.

A Page Object example

This is a Page Object that navigates to a URL and enable you to fill out a form and return a confirmation message. It also verifies that it is on the correct page. At least that the page title is the expected.

src/test/java/se/thinkcode/RequestNewPasswordPage.java

package se.thinkcode;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class RequestNewPasswordPage {
    private WebDriver browser;

    public RequestNewPasswordPage(WebDriver browser) {
        this.browser = browser;
        String expectedTitle = "Request new password";

        String currentUrl = browser.getCurrentUrl();
        String url = currentUrl + "requestPassword";

        browser.get(url);

        String actualTitle = browser.getTitle();

        assertThat(actualTitle, is(expectedTitle));
    }

    public String requestNewPassword(String userName) {
        WebElement userNameField = browser.findElement(By.id("account"));
        userNameField.sendKeys(userName);

        userNameField.submit();

        PasswordRequestedPage passwordRequested = new PasswordRequestedPage(browser);

        return passwordRequested.getConfirmation();
    }
}

The important thing if I want to test this form is not to locate fields or similar. The most important thing is to submit the form with the right values. And get the result back. A test shouldn't fail if I decide to name a widget differently. Changing the name of the input field for example. An update of the page object will make sure that all tests continue to work as expected.

A test that uses Page Objects

The test should focus on the expected behavior and nothing else. Navigation is not interesting for the behaviour so I don't want to see any navigation in a test.

src/test/java/se/thinkcode/RequestNewPasswordTest.java

@Test
public void request_new_password() {
    String expected = "A new password has been sent to Thomas Sundberg";
    RequestNewPasswordPage newPasswordPage = new RequestNewPasswordPage(browser);

    String actual = newPasswordPage.requestNewPassword("Thomas Sundberg");

    assertThat(actual, is(expected));
}

Separating navigation and verification is easy and will greatly improve your life.

If you are interested in learning more about Selenium, please join me in Timisoara, Romania in April 2016 for a two day course on testing web applications with Selenium in cooperation with Mozaic Works.

Complete sources

The complete source is included below. The files are organized as:

example
|-- pom.xml
`-- src
    `-- test
        `-- java
            `-- se
                `-- thinkcode
                    |-- PasswordRequestedPage.java
                    |-- RequestNewPasswordPage.java
                    `-- RequestNewPasswordTest.java

src/test/java/se/thinkcode/RequestNewPasswordTest.java

package se.thinkcode;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class RequestNewPasswordTest {
    private WebDriver browser;

    @Before
    public void setUp() {
        String host = "http://selenium.thinkcode.se";
        browser = new FirefoxDriver();
        browser.get(host);
    }

    @After
    public void tearDown() {
        browser.quit();
    }

    @Test
    public void request_new_password() {
        String expected = "A new password has been sent to Thomas Sundberg";
        RequestNewPasswordPage newPasswordPage = new RequestNewPasswordPage(browser);

        String actual = newPasswordPage.requestNewPassword("Thomas Sundberg");

        assertThat(actual, is(expected));
    }
}

src/test/java/se/thinkcode/RequestNewPasswordPage.java

package se.thinkcode;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class RequestNewPasswordPage {
    private WebDriver browser;

    public RequestNewPasswordPage(WebDriver browser) {
        this.browser = browser;
        String expectedTitle = "Request new password";

        String currentUrl = browser.getCurrentUrl();
        String url = currentUrl + "requestPassword";

        browser.get(url);

        String actualTitle = browser.getTitle();

        assertThat(actualTitle, is(expectedTitle));
    }

    public String requestNewPassword(String userName) {
        WebElement userNameField = browser.findElement(By.id("account"));
        userNameField.sendKeys(userName);

        userNameField.submit();

        PasswordRequestedPage passwordRequested = new PasswordRequestedPage(browser);

        return passwordRequested.getConfirmation();
    }
}

src/test/java/se/thinkcode/PasswordRequestedPage.java

package se.thinkcode;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class PasswordRequestedPage {
    private final WebDriver browser;

    public PasswordRequestedPage(WebDriver browser) {
        this.browser = browser;
        String expectedTitle = "Confirm new password request";

        String actualTitle = browser.getTitle();

        assertThat(actualTitle, is(expectedTitle));
    }

    public String getConfirmation() {
        WebElement confirmation = browser.findElement(By.id("confirmation"));

        return confirmation.getText();
    }
}

A Maven project file that ties everything together looks like this:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.thinkcode</groupId>
    <artifactId>example</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>2.48.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Acknowledgements

I would like to thank Malin Ekholm for proof reading.

References



(less...)

Pages

About
Events
Why

Categories

Agile
Automation
BDD
Clean code
Continuous delivery
Continuous deployment
Continuous integration
Cucumber
Culture
Design
DevOps
Executable specification
Git
Gradle
Guice
J2EE
JUnit
Java
Javascript
Kubernetes
Linux
Load testing
Maven
Mockito
New developers
Pair programming
PicoContainer
Presentation
Programming
Public speaking
Quality
React
Recruiting
Requirements
Scala
Selenium
Software craftsmanship
Software development
Spring
TDD
Teaching
Technical debt
Test automation
Tools
Web
Windows
eXtreme Programming

Authors

Thomas Sundberg
Adrian Bolboaca

Archives

Meta

rss RSS