Selenium from scratch

Hi reader,

If you’re concerned with testing then you’ve already came accross the difficult task of UI testing.
We all agree that fat client model is different from thin client’s but they share common characteristics: screens and transitions between screens. Testing them is different but seems equaly difficult. We will cover thin UI ie web site pages testing.
Before thinking about the “how”, we’ll think of the “what”.
Every UI is a set of screens (we pages in our case). Pages display data in UI components. Displaying data as a result of an action is something we might want to test:

Given some persisted messages
When I navigate to the "list messages" page
Then the UI should display the persisted messages

Given I input the "create message" form with invalid phone number
When I submit the "create message" form
Then the UI should display "Invalid phone number" error

The other very important behaviour to test is the transitions between screens.
In an HTTP context, requesting a resource with the “GET” method might not be allowed. In a security context, accessing a resource might not be allowed at all. From the UI perspective preventing a transition from screen A to B often translates into removing/hidding a link/button from the page:

Given I provide "admin/admin" authentication
When I navigate to the "catalog"
Then the page should display the "delete" link

Given I provide "user/user" authentication
When I navigate to the "catalog"
Then the page should not display the "delete" link

We will test 2 transitions: a successful form submission and a failure.

Selenium is a tool that allows some arbitrary piece of code to describe a behaviour against a target browser. The behaviour is executed on the specified browser and the execution result (often a page) is collected by Selenium. The code can then verify assertions.
We could reach the same results with a lot more efforts: use http-components, send http request, parse response, provide elements location mechanism. This is all provided by Selenium and even more.
With Selenium we can :
– navigate to a page: acts as starting point,
– assert page identity: web elements (links, buttons, form, etc). Location is an important part of the tool. We can then assert the page identity based on page structure (controls, links, forms presence). Any element in the dom can be selected by xpath, css, name, id.
– simulate form submission

Lets build a simple example that illustrates the concepts.

First create a web project.

$ mvn archetype:generate | grep thymeleaf
45: remote -> com.lordofthejars.thymeleafarchetype:thymeleaf-spring-maven-archetype (Thymeleaf Spring Maven Archetype)

Re-run mvn archetype:generate, choose the right number (45 in my example) and follow the instructions. I chosed “selenium-from-scratch” as project name.

Check that everything runs fine

$ cd selenium-from-scratch
$ mvn idea:idea
$ mvn clean verify
$ mvn tomcat:run

Configure maven-jetty-plugin and maven-failsafe-plugin to run ITTests during ‘integration-test’ phase: start jetty on ‘pre-integration-test’ phase, run *FunctionnalTest on ‘integration-test’ then stop jetty on ‘post-integration-test’.

            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>maven-jetty-plugin</artifactId>
                <version>${maven-jetty-plugin.version}</version>
                <configuration>
                    <contextPath>${project.build.finalName}</contextPath>
                    <connectors>
                        <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
                            <port>9090</port>
                            <maxIdleTime>60000</maxIdleTime>
                        </connector>
                    </connectors>
                    <scanIntervalSeconds>10</scanIntervalSeconds>
                    <stopKey>foo</stopKey>
                    <stopPort>9180</stopPort>
                    <webApp>${project.build.directory}/${project.build.finalName}.war</webApp>
                    <daemon>true</daemon>
                </configuration>
                <executions>
                    <execution>
                        <id>start-jetty</id>
                        <goals>
                            <goal>deploy-war</goal>
                        </goals>
                        <phase>pre-integration-test</phase>
                    </execution>
                    <execution>
                        <id>stop-jetty</id>
                        <goals>
                            <goal>stop</goal>
                        </goals>
                        <phase>post-integration-test</phase>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>${maven-failsafe-plugin.version}</version>
                <configuration>
                    <includes>
                        <include>%regex[.*FunctionnalTest.*]</include>
                    </includes>
                    <encoding>UTF-8</encoding>
                </configuration>
                <executions>
                    <execution>
                        <id>integration-test</id>
                        <goals>
                            <goal>integration-test</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>verify</id>
                        <goals>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

In order to write any test we need to configure Selenium in our pom.xml

        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>${selenium.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-support</artifactId>
            <version>${selenium.version}</version>
            <scope>test</scope>
        </dependency>

Our tests configuration is now ready. Let’s write some tests.

Say your side provides a contact page and we want to test a successful message submission:

Given I navigate to the "create message" page
When I input valid message data
And I send form
Then the outcome page should be the "list messages"  page

An implementation chould be:

...
  private WebDriver driver;

  @Before
  public void before() throws Exception {
    if (driver != null) driver.quit();
    driver = new FirefoxDriver();
  }

  @Test
  public void createMessageShouldSucceedAndListMessages() {
    CreateMessagePage createMessagePage = PageFactory.initElements(driver, CreateMessagePage.class);
    createMessagePage.visit();
    Message message = TestFixtures.validMessage();
    createMessagePage.fillCreateMessageForm(message);
    ListMessagesPage
        listMessagesPage =
        (ListMessagesPage) createMessagePage.sendCreateMessageForm(false);
    listMessagesPage.assertContainsExpectedName(message.getName());
    listMessagesPage.assertContainsExpectedEmail(message.getEmail());
    listMessagesPage.assertContainsExpectedPhone(message.getPhone());
    listMessagesPage.assertContainsExpectedMessage(message.getMessage());
    listMessagesPage.assertContainsExpectedWebsite(message.getWebsite());
  }
...

The test is ‘page centric’ and follows the PageObject pattern recommended by the Selenium team. They provide a way of creating pages via PageFactory.initElements method. The page class provides a single WebDriver argument constructor.
The pages hold their own url and navigate thanks to driver.get method.

  public void visitInternal() {
    getDriver().get(BASE_URL +  "/list");
  }

They also assert identity by inspecting the navigation result. It should match a set of expected conditions. The example below uses the page title as identifier: it is far too weak but it illustrates the concept.

  public void assertIdentity() {
    final String listMessagesPageTitle = "dive into jee :: list messages";
    assertTrue(getDriver().getTitle().equalsIgnoreCase(listMessagesPageTitle));
  }

The driver provides methods to interact with forms like humans would. We first make sure the element is on the page, then we use it.
Note the use of the sendKeys method of each element:

  public void fillCreateMessageForm(Message message) {
    WebElement name_input = getDriver().findElement(By.id("name"));
    assertNotNull(name_input);
    name_input.sendKeys(message.getName());
    WebElement email_input = getDriver().findElement(By.id("email"));
    assertNotNull(email_input);
    email_input.sendKeys(message.getEmail());
    WebElement phone_input = getDriver().findElement(By.id("phone"));
    assertNotNull(phone_input);
    phone_input.sendKeys(message.getPhone());
    WebElement website_input = getDriver().findElement(By.id("website"));
    assertNotNull(website_input);
    website_input.sendKeys(message.getWebsite());
    WebElement message_input = getDriver().findElement(By.id("message"));
    assertNotNull(message_input);
    message_input.sendKeys(message.getMessage());
  }

Once filled, the form can be sent. There can be 2 outcomes for a form submission; failure or success:

  public Page sendCreateMessageForm(boolean failureExpected) {
    form.submit();
    Page page;
    if (failureExpected) {
      page = PageFactory.initElements(getDriver(), CreateMessagePage.class);
    } else {
      page = PageFactory.initElements(getDriver(), ListMessagesPage.class);
    }
    page.assertIdentity();
    return page;
  }

You might wonder where the form element has been initialized. Well, selenium-support provides elements location mechanism with the @FindBy annotation. A lazy proxy that implements WebElement is injected into fields during PageFactory.initElements method. The actual finding will occur at runtime. If not located, a NoSuchElementException is raised.

...
  @FindBy(id = "create-message-form")
  WebElement form;
...

Once the outcome page is loaded we can assert its identity and continue our navigation or verify page content:

public class ListMessagesPage extends Page {
...
  public void assertContainsExpectedEmail(String email) {
    String emailXpathExpression = "//table[@id='display-data']//td[text()='" + email + "']";
    WebElement emailNode = getDriver().findElement(By.xpath(emailXpathExpression));
    assertNotNull(emailNode);
  }
...
}

The same principle has been applied to test the failure outcome which is another transition.

The sample would not be complete without the html pages:

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<head>
...
<title>Dive into jee :: create message</title>
...
</head>

...
    <form id="create-message-form" action="#" th:object="${messageInfo}" th:action="@{/insert}"
          method="post">

        <h1 th:text="#{form.title}">Get In Touch With Us ...</h1>

        <fieldset id="user-details">

            <label for="name" th:text="#{theader.name}">Name: </label>
            <input type="text" name="name"
                   value="" id="name" placeholder="Your Name" required="required"
                   autofocus="autofocus" th:value="*{name}"/>
            <label for="email" th:text="#{theader.email}">Email: </label>
            <input type="email" name="email" value="" id="email" placeholder="mail@email.com"
                   required="required" th:value="*{email}"/>
            <label for="phone" th:text="#{theader.phone}">Phone: </label>
            <input type="text" id="phone" name="phone" value="" th:value="*{phone}"/>
            <label for="website" th:text="#{theader.website}">Website:</label>
            <input type="url" id="website" name="website" value="" th:value="*{website}"/>

        </fieldset>
        <!--end user-details-->

        <fieldset id="user-message">

            <label for="message" th:text="#{theader.message}">Your Message: </label>
            <textarea id="message" name="message" rows="0" cols="0" th:value="*{message}"></textarea>

            <input type="submit" value="Submit Message" name="submit"
                   class="submit"/>
        </fieldset>
        <!-- end user-message -->
        <div id="errors" th:if="${#fields.hasErrors('*')}">
            <ul>
                <li th:each="err : ${#fields.errors('*')}" th:text="${err}">Input is incorrect</li>
            </ul>
        </div>
    </form>
...

</html>
<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

<head>
...
<title>Dive into jee :: list messages</title>
...
</head>

<body>
	<div id="container">
		<table id="display-data">
			<thead>
				<tr>
					<th th:text="#{theader.count}"></th>
					<th th:text="#{theader.name}"></th>
					<th th:text="#{theader.email}"></th>
					<th th:text="#{theader.phone}"></th>
					<th th:text="#{theader.website}"></th>
					<th th:text="#{theader.message}"></th>
				</tr>
			</thead>
			<tbody>
				<tr th:each="msg, rowStat : ${messages}">
					<td th:text="${rowStat.count}">1</td>
					<td th:text="${msg.name}">My Name</td>
					<td th:text="${msg.email}">my@email.com</td>
					<td th:text="${msg.phone}">9999</td>
					<td th:text="${msg.website}">http://mysite.com</td>
					<td th:text="${msg.message}">my message</td>
				</tr>
			</tbody>
		</table>
	</div>

...
</body>

</html>

There we are: we have a functionnnal selenium testing environnement. We can add tests, note failures, edit html pages, create server side code and make tests pass.

But we’ve barely scratched the surface of Selenium. There is more.
Selenium tests tend to be quite verbose, specially in java. Consequently 2 framework emerged to fix that verbosity: fluent-selenium (Paul Hammant) and FluentLenium (Mathilde Lemée).
Selenium tests tend to be quite resource greedy. The Selenium team started selenium-grid to distribute test executions thus lightening/speeding heavy builds.

I think anyone interested in web UI testing should give it a try. The first results are really satisfying but the true challenge comes with volume:
– how will your CI platform scale with dozens of Selenium tests?
– which tecnniques to keep a fast build and a low memory footprint?
Selenium provides the selenium-grid project to address those issues. It uses a master that configures/drives the execution plan and slave that actually execute the tests.
I might save this topic for another post (or not). Just keep in mind that configuring selenium-grid is simple and can save you time even if it comes at a cost: you need to have a few machines available for testing.
I like the tool but before using Grid I would isolate Selenium tests in a maven profile and run them on a dedicated CI server.

I wrote this article for those like me who had the false idea that Selenium is a heavy tool for heavy sites. It’s not true, one can use it at the very first page of your website and integrate it at every story.

You can have a look at the entire code and provide any useful insight.

Advertisements

2 thoughts on “Selenium from scratch

    1. The term “fat client” often refers to any piece of software that has a graphical user interface and doesn’t run on a web browser. This excludes command line programs and websites (thin client).
      Well know fat clients are destop application such as gnome nautilus, thunderbird, sublime, etc.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s