Testing a web application with Selenium 2

Filed under: Automation, Cucumber, Maven, Selenium, Software development, Test automation, — Tags: BDD, Behaviour Driven Development, Behaviour Driven Development - BDD, Cucumber, Cucumber-jvm, Executable specifications, Java, Page Object, Parameterized JUnit, WebDriver — Thomas Sundberg — 2011-10-18

Selenium a great tool for testing web applications. The current version, Selenium 2, is a merge between Selenium and WebDriver. I will walk you through an example where we test a web site using Selenium in a few different ways. This is the same example as I demonstrated at Scandev on tour in Stockholm 18 October 2011.

Selenium IDE

Selenium IDE is a record and play tool available as a plugin to Firefox. We will start with it and record a simple search scenario.

Start Selenium IDE and follow these steps:

Try to replay the scenario and notice that it navigates the same way as you just did. The largest strength with a tool like Selenium is, however, it's possibilities to be developed and maintained in a programming language. Let's export the current test case to Java and use it as a starting point for something that may be robuster and easier to maintain in the long run.

Export the test case as JUnit 4 (WebDriver) and save the file as SearchExportedFromIDETest.java in the directory src/test/java

The file location indicates that this will be a Maven project. A Maven pom is needed and one that is sufficient at this stage may look like:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.sigma.selenium</groupId>
    <artifactId>minimal</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>2.29.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Back to the exported file. It looks like this:

src/test/java/SearchExportedFromIDETest.java

package com.example.tests;

import java.util.regex.Pattern;
import java.util.concurrent.TimeUnit;
import org.junit.*;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.openqa.selenium.*;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.Select;

public class SearchExportedFromIDETest {
	private WebDriver driver;
	private String baseUrl="";
	private StringBuffer verificationErrors = new StringBuffer();
	@Before
	public void setUp() throws Exception {
		driver = new FirefoxDriver();
		driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
	}

	@Test
	public void testSearchExportedFromIDE() throws Exception {
		driver.get("/");
		driver.findElement(By.id("sted")).clear();
		driver.findElement(By.id("sted")).sendKeys("Stockholm");
		driver.findElement(By.id("queryknapp")).click();
		driver.findElement(By.xpath("//div[@id='directories']/table/tbody/tr/td[2]/a")).click();
		driver.findElement(By.cssSelector("li")).click();
		assertEquals("Weather forecast forStockholm (Sweden)", driver.findElement(By.cssSelector("h1")).getText());
	}

	@After
	public void tearDown() throws Exception {
		driver.quit();
		String verificationErrorString = verificationErrors.toString();
		if (!"".equals(verificationErrorString)) {
			fail(verificationErrorString);
		}
	}

	private boolean isElementPresent(By by) {
		try {
			driver.findElement(by);
			return true;
		} catch (NoSuchElementException e) {
			return false;
		}
	}
}

It needs some cleaning up. Note that it is not good enough to be executed, it is among other things not placed in the correct package and it doesn't have an address to the system under test set, the web application that should be tested. But it is a starting point.

Selenium WebDriver

We are now done with the IDE part. Selenium IDE is a great tool to record a scenario, but it should only be used as a starting point.

Some cleaning of the exported file will make it look something like this:

src/test/java/se/sigma/selenium/ide/SearchExportedFromIDETest.java

package se.sigma.selenium.ide;

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

import java.util.concurrent.TimeUnit;

import static junit.framework.Assert.assertTrue;

public class SearchExportedFromIDETest {
    private WebDriver driver;

    @Before
    public void setUp() throws Exception {
        driver = new FirefoxDriver();
        driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
    }

    @After
    public void tearDown() throws Exception {
        driver.quit();
    }

    @Test
    public void testSearchExportedFromIDE() throws Exception {
        String baseUrl = "http://www.yr.no";
        driver.get(baseUrl + "/");

        WebElement searchField = driver.findElement(By.id("sted"));
        searchField.clear();
        searchField.sendKeys("Stockholm");
        searchField.submit();

        String topLinkXPathExpression = "//div[@id='directories']/table/tbody/tr/td[2]/a";
        WebElement topSearchResult = driver.findElement(By.xpath(topLinkXPathExpression));
        topSearchResult.click();

        driver.findElement(By.cssSelector("li")).click();
        String expected = "Stockholm";
        WebElement actualHeadLine = driver.findElement(By.cssSelector("h1"));
        String actual = actualHeadLine.getText();
        assertTrue(actual.contains(expected));
    }
}

It has been transformed into something that is executable and that may be easier to read. We could leave it like this, but then we wouldn't be better than the average developer. Since we are better, we will try to make this example even more readable.

Something more readable might look like this:

src/test/java/se/sigma/selenium/po/SearchTest.java

package se.sigma.selenium.po;

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 junit.framework.Assert.assertTrue;

public class SearchTest {
    private WebDriver driver;

    @Before
    public void setUp() {
        driver = new FirefoxDriver();
    }

    @After
    public void tearDown() {
        driver.close();
    }

    @Test
    public void verifyThatStockholmCanBeFound() {
        HomePage home = new HomePage(driver);
        SearchResultPage searchResult = home.searchFor("Stockholm");

        LocationPage stockholm = searchResult.clickOnTopSearchResultLink();

        String expected = "Stockholm";
        String actual = stockholm.getHeadLine();
        assertTrue(actual.contains(expected));
    }
}

This implementation need some support classes and they will be an implementation of the Page Object design pattern.

Page Object Pattern

The idea behind the Page Object Pattern is that there should be one class that represents everything one page can do. If this object is used whenever any interaction with that page occurs, there will be just one place in test code that need to be maintained whenever a page changes. It is an attempt to make test less fragile.

The classes needed are:

HomePage

src/main/java/se/sigma/selenium/po/HomePage.java

package se.sigma.selenium.po;

import org.apache.commons.io.FileUtils;
import org.openqa.selenium.*;

import java.io.File;
import java.io.IOException;

public class HomePage {
    private WebDriver driver;

    public HomePage(WebDriver driver) {
        this.driver = driver;
        String baseUrl = "http://www.yr.no";
        driver.get(baseUrl + "/");
    }

    public SearchResultPage searchFor(String location) {
        try {
            WebElement searchField = driver.findElement(By.id("sted"));
            searchField.clear();
            searchField.sendKeys(location);
            searchField.submit();
        } catch (RuntimeException e) {
            takeScreenShot(e, "searchError");
        }

        return new SearchResultPage(driver);
    }

    private void takeScreenShot(RuntimeException e, String fileName) {
        File screenShot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        try {
            FileUtils.copyFile(screenShot, new File(fileName + ".png"));
        } catch (IOException ioe) {
            throw new RuntimeException(ioe.getMessage(), ioe);
        }
        throw e;
    }
}

SearchResultPage

src/main/java/se/sigma/selenium/po/SearchResultPage.java

package se.sigma.selenium.po;

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

public class SearchResultPage {
    private WebDriver driver;

    public SearchResultPage(WebDriver driver) {
        this.driver = driver;
        if (!driver.getTitle().contains("yr.no")) {
            throw new IllegalStateException("This is not yr.no: " + driver.getCurrentUrl());
        }
    }

    public LocationPage clickOnTopSearchResultLink() {
        String topLinkXPathExpression = "//div[@id='directories']/table/tbody/tr/td[2]/a";
        WebElement topResultLink = driver.findElement(By.xpath(topLinkXPathExpression));
        topResultLink.click();

        return new LocationPage(driver);
    }
}

LocationPage

src/main/java/se/sigma/selenium/po/LocationPage.java

package se.sigma.selenium.po;

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

public class LocationPage {
    private WebDriver driver;

    public LocationPage(WebDriver driver) {
        this.driver = driver;

        if (!driver.getTitle().contains("yr.no")) {
            throw new IllegalStateException("This is not yr.no: " + driver.getCurrentUrl());
        }
    }

    public String getHeadLine() {
        WebElement resultPageHeadLine = driver.findElement(By.cssSelector("h1"));
        return resultPageHeadLine.getText();
    }
}

Take a screen shot on failure

Selenium 2 has the capability to save an image of the current browser. This may come very handy if a test fails. An example of an implementation is included in the HomePage implementation above. The specific implementation is this snippet:

private void takeScreenShot(RuntimeException e, String fileName) {
    File screenShot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
    try {
        FileUtils.copyFile(screenShot, new File(fileName + ".png"));
    } catch (IOException ioe) {
        throw new RuntimeException(ioe.getMessage(), ioe);
    }
    throw e;
}

This implementation is a small example on how it can be done. How you invoke it in your test project is up to you. I would consider to capture the browser every time an unexpected exception is thrown. How I would do it would depend on how I drive the browser.

Increased readability

The Page Object design pattern represents a good step in the direction of maintainability. But it is still hard to read for people who doesn't care much about code. A way to increase the readability could be to use another tool to define the test. One such tool could be Cucumber.

Cucumber is a tool for Behaviour Driven Development, BDD. Or if you want Executable Specification or Specifications by Examples.

It works like this:

In this example, a feature could look like this:

src/test/resources/se/sigma/selenium/cu/Search.feature

Feature: It should be possible to search for places at the Norwegian Meteorological Institute, http://www.yr.no

Scenario: Locate Stockholm

    Given I want to know the weather forecast for coming days
    When I search for Stockholm
    Then I should be able to get a weather forecast for Stockholm

The format is

This format is easy to read and easy to discuss with people who understands the business and the reasons why we even develop the particular software.

To execute the specification above, we need to connect the feature with some code. It is done using a specific step definition class and a specific JUnit test class.

A JUnit test that connects the feature with some steps may look like this:

src/test/java/se/sigma/selenium/cu/SearchTest.java

package se.sigma.selenium.cu;

import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
public class SearchTest {
}

The steps that actually can execute the specification may look like this:

src/test/java/se/sigma/selenium/cu/SearchStepDefinitions.java

package se.sigma.selenium.cu;


import cucumber.api.java.After;
import cucumber.api.java.Before;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import se.sigma.selenium.po.HomePage;
import se.sigma.selenium.po.LocationPage;
import se.sigma.selenium.po.SearchResultPage;

import static junit.framework.Assert.assertTrue;

public class SearchStepDefinitions {
    private WebDriver driver;
    private HomePage home;
    private SearchResultPage searchResult;

    @Before
    public void prepare() {
        driver = new FirefoxDriver();
    }

    @After
    public void cleanUp() {
        driver.close();
    }

    @Given("^I want to know the weather forecast for coming days$")
    public void prepareHomePage() {
        home = new HomePage(driver);
    }

    @When("^I search for (.*)$")
    public void search(String location) {
        searchResult = home.searchFor(location);
    }

    @Then("^I should be able to get a weather forecast for (.*)$")
    public void assertTheSearchResult(String locationName) {
        LocationPage location = searchResult.clickOnTopSearchResultLink();
        String actualHeadLine = location.getHeadLine();

        assertTrue(actualHeadLine.contains(locationName));
    }
}

The step definitions above may be harder to read compared to the JUnit code that executed the Page Object Pattern above. But the part that should be read by non developers are the features and they are definitely more readable then the JUnit code.

File structure and Maven

The file structure used in this example is:

example
|-- pom.xml
`-- src
    |-- main
    |   `-- java
    |       `-- se
    |           `-- sigma
    |               `-- selenium
    |                   `-- po
    |                       |-- HomePage.java
    |                       |-- LocationPage.java
    |                       `-- SearchResultPage.java
    `-- test
        |-- java
        |   `-- se
        |       `-- sigma
        |           `-- selenium
        |               |-- cu
        |               |   |-- SearchStepDefinitions.java
        |               |   `-- SearchTest.java
        |               |-- ide
        |               |   `-- SearchExportedFromIDETest.java
        |               `-- po
        |                   `-- SearchTest.java
        `-- resources
            `-- se
                `-- sigma
                    `-- selenium
                        `-- cu
                            `-- Search.feature

The final piece that is needed to execute this example is a Maven pom. It defines all dependencies needed and may look like this:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.sigma.selenium</groupId>
    <artifactId>example</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>2.29.1</version>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>1.1.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>1.1.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Conclusion

Selenium 2 is a tool that can be used to verify web sites. It is a merge between Selenium 1 and WebDriver. Code generated out of the box from Selenium IDE is not really nice. It is better to evolve it and implement the Page Object design pattern.

Reading tests is difficult if you don't like reading code. Defining the specification using Cucumber may be a better way. Cucumber can then be used to execute the tests.

Acknowledgements

This post has been reviewed by some people whom I wish to thank for their help

Thank you very much for your feedback!

Resources



(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