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.
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.
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.
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>
I would like to thank Malin Ekholm for proof reading.