Selenium testing your GWT application using maven

Selenium is a great tool to do browser based testing of your web user interface. While it can be a bit of a pain to set up properly, it is fun to see your application being used very fast in the test and comforting to know that your user interface logic is also verified by your continuous integration system.

There are two important steps in the process. You need to get your maven configuration running and you have to write the actual test.

For this example, we are showing a selenium test which verifies the security aspects in Geomajas (a GIS application framework) using the staticsecurity plug-in. This example verifies a small GWT application which uses the SmartGWT widget library. The running test looks like this:

Maven configuration

To be able to use Selenium for the test, you need to include some dependencies.

<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-java</artifactId>
  <version>2.5.0</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-server</artifactId>
  <version>2.5.0</version>
  <scope>test</scope>
</dependency>

To run the selenium tests, the web application has to be runnable. I did this by configuring jetty. This is done as a build plug-in and allows “mvn jetty:run” to work. As this is a GWT application, you need to add a location to assure the GWT compiled stuff is included in the application.

<plugin>
  <groupId>org.mortbay.jetty</groupId>
  <artifactId>maven-jetty-plugin</artifactId>
  <version>6.1.20</version>
  <configuration>
    <webAppConfig>
      <contextPath>/</contextPath>
      <baseResource implementation="org.mortbay.resource.ResourceCollection">
        <!-- need both the webbapp dir and location where GWT puts stuff -->
        <resourcesAsCSV>
          ${basedir}/src/main/webapp,${project.build.directory}/${project.build.finalName}
        </resourcesAsCSV>
      </baseResource>
    </webAppConfig>
    <reload>manual</reload>
  </configuration>
</plugin>

I normally put the selenium tests in a profile to allow your build to run faster by switching off the selenium tests.
The profile is defined below. You can disable the selenium tests by including “-DskipSelenium” on the maven command line. The actual steps are included as build plug-ins in the profile. Please beware that this type of configuration will cause profiles which are indicates as activeByDefault to be disabled.

<profile>
  <id>selenium-tests</id>
  <activation>
    <property>
      <name>!skipSelenium</name>
    </property>
  </activation>
  <build>
    <plugins>
      <!-- plug-ins come here -->
    </plugins>
  </build>
</profile>

The selenium tests are run in the integration test phase.
We need to assure the application is started. We will start the application using the jetty servlet engine.
The most important part are the executions. There is one execution to start jetty in the pre-integration-test phase and another to stop jetty in the post-integration-test phase. The jetty runs on port 9080 instead of 8080. This is not required but is done to prevent clashes when working on several projects at the same time.

<plugin>
  <groupId>org.mortbay.jetty</groupId>
  <artifactId>maven-jetty-plugin</artifactId>
  <configuration>
    <webAppConfig>
      <contextPath>/</contextPath>
    </webAppConfig>
    <reload>manual</reload>
    <stopPort>9966</stopPort>
    <stopKey>stop-jetty</stopKey>
  </configuration>
  <executions>
    <execution>
      <id>start-jetty</id>
      <phase>pre-integration-test</phase>
      <goals>
        <goal>run</goal>
      </goals>
      <configuration>
        <daemon>true</daemon>
        <scanIntervalSeconds>5</scanIntervalSeconds>
        <connectors>
          <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
            <port>9080</port>
            <maxIdleTime>60000</maxIdleTime>
          </connector>
        </connectors>
      </configuration>
    </execution>
    <execution>
      <id>stop-jetty</id>
      <phase>post-integration-test</phase>
      <goals>
        <goal>stop</goal>
      </goals>
    </execution>
  </executions>
</plugin>

The tests are actually run by the surefire plug-in. Integration tests are marked as such in the class name. This is done by making the class name start with “IntTest” or end in “TestInt” (you can change this pattern, but is is recommended not to make it start or end in “Test” to prevent the need for special configuration to exclude it from the test phase).

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <executions>
    <execution>
      <phase>integration-test</phase>
      <goals>
        <goal>test</goal>
      </goals>
      <configuration>
        <includes>
          <include>**/*TestInt.java</include>
          <include>**/IntTest*.java</include>
        </includes>
      </configuration>
    </execution>
  </executions>
</plugin>

Note that no server side component needs to run to execute the test itself, just the web application. So while developing, you could just as well keep the application running (for example by using “mvn jetty:run”) and just run the test itself from your IDE.

You can have a look at the full pom here.

Test code

For the test, I will use the selenium driver to connect with the web browser. This is initialised using the code below. You can use different implementation of WebDriver to switch between different browsers. There is also a version which does not use a browser at all. In this case, I am using the FirefoxDriver.

private WebDriver driver;

@Autowired
private CommandCountAssert commandCountAssert;

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

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

Some constants have been defined for the string which are used in the test.

private static final String LAYER_VECTOR = "-clientLayerCountries";
private static final int LAYER_VECTOR_LENGTH = LAYER_VECTOR.length() - 1;
private static final String LAYER_VECTOR_XPATH =
  "//*[substring(@id, string-length(@id)-" + LAYER_VECTOR_LENGTH + ")= '" + LAYER_VECTOR + "']";
private static final String LAYER_RASTER = "-clientLayerOsm";
private static final int LAYER_RASTER_LENGTH = LAYER_RASTER.length() - 1;
private static final String LAYER_RASTER_XPATH =
  "//*[substring(@id, string-length(@id)-" + LAYER_RASTER_LENGTH + ")= '" + LAYER_RASTER + "']";

As you could see from the clip at the beginning, the test verifies many things. Let’s look at this is small steps.

To begin, we need to initialise some objects which do the actual testing later on. The wait service allows you to wait for something to appear in the DOM tree. It is configured to wait at most 20 seconds. There is also a commandCountAssert service which is initialised. This is a spring service which counts the command interactions.

String source;
List<WebElement> elements;
WebDriverWait wait = new WebDriverWait(driver, 20);
wait.pollingEvery(500, TimeUnit.MILLISECONDS);
commandCountAssert.init();

Now connect to the actual application. The port here matches the port from the jetty configuration in the pom.

driver.get("http://localhost:9080/");

We are testing the security aspects and the program should immediately display a login window. Verify this by waiting for the window to appear. We do this by trying to select the object using the class name.

// the login window should appear
wait.until(new ExpectedCondition<Boolean>() {
  public Boolean apply(WebDriver d) {
    return null != d.findElement(By.className(TokenRequestWindow.STYLE_NAME_WINDOW));
  }
});

We also want to check that only only one command was sent to the server and that the other commands are queued for when the authentication is done.

commandCountAssert.assertEquals(1);

We need to find some elements in the DOM tree for this. The “aria-label” XPath expressions is required because of how SmartGWT handles buttons. This actually finds the button with the given label. We start by just finding the useful bits from the login window.

WebElement userName = driver.findElement(By.name("userName"));
WebElement password = driver.findElement(By.name("password"));
WebElement login = driver.findElement(By.xpath("//*[@aria-label='Log in']"));
WebElement reset = driver.findElement(By.xpath("//*[@aria-label='Reset']"));

Let’s fill the login window with some invalid credentials. We fill in the user name and password fields and click to login. We then wait for the “Login attempt has failed” message.

userName.sendKeys("blabla");
password.sendKeys("blabla");
login.click();
wait.until(new ExpectedCondition<Boolean>() {
  public Boolean apply(WebDriver d) {
    return d.findElement(By.className(TokenRequestWindow.STYLE_NAME_ERROR)).getText().
        contains("Login attempt has failed");
  }
});

Verify that only the login attempt command was sent.

commandCountAssert.assertEquals(1);

Now clear the form and check that the error message is also cleared.

WebElement error = driver.findElement(By.className(TokenRequestWindow.STYLE_NAME_ERROR));
reset.click();
Assert.assertEquals("", error.getText());

Make sure that the correct error is displayed when no user name is specified.

reset.click();
userName.clear();
password.sendKeys("luc");
login.click();
wait.until(new ExpectedCondition<Boolean>() {
  public Boolean apply(WebDriver d) {
    return null != d.findElement(By.xpath("//*[contains(.,'Please fill in a user name.')]"));
  }
});

And this should be entirely client-side.

commandCountAssert.assertEquals(0);

Similarly, trying to login without password should also fail.

reset.click();
userName.sendKeys("luc");
password.clear();
login.click();
wait.until(new ExpectedCondition<Boolean>() {
  public Boolean apply(WebDriver d) {
    return null != d.findElement(By.xpath("//*[contains(.,'Please fill in a password.')]"));
  }
});
commandCountAssert.assertEquals(0);

Now pass some valid credentials.

reset.click();
userName.sendKeys("luc");
password.sendKeys("luc");
login.click();

The application title should be displayed and the map has to appear.

wait.until(new ExpectedCondition<Boolean>() {
  public Boolean apply(WebDriver d) {
    return null != d.findElement(By.className(Application.APPLICATION_TITLE_STYLE));
  }
});
wait.until(new ExpectedCondition<Boolean>() {
  public Boolean apply(WebDriver d) {
    List<WebElement> elements = driver.findElements(By.xpath(LAYER_VECTOR_XPATH));
    return !elements.isEmpty();
  }
});

Logging in should display the user name on screen. It should also display a raster layer and a “blabla” button. For the button it checks that it is actually visible (when the button is removed, SmartGWT will hide it, not remove it from the DOM tree).

WebElement user = driver.findElement(By.className(Application.APPLICATION_USER_STYLE));
Assert.assertEquals("user: Luc Van Lierde", user.getText());
elements = driver.findElements(By.xpath(LAYER_RASTER_XPATH));
Assert.assertFalse(elements.isEmpty()); // there should be a raster layer
WebElement blabla = driver.findElement(By.xpath("//*[@aria-label='blabla']")); 
Assert.assertNotNull(blabla); // should exist
Assert.assertFalse(blabla.getAttribute("style").contains("visibility: hidden"));

Now check that the login window disappeared. We search for the elements with the class name of the login window and verify that nothing exists.

Assert.assertEquals(0, driver.findElements(By.className(TokenRequestWindow.STYLE_NAME_WINDOW)).size());

With all this work, somewhere between 20 and 40 commands should have been executed (the exact number depends on screen size).

commandCountAssert.assertBetween(20, 40);

Now logout again. This should make the login window appear again. The layers in the map should have disappeared and two commands should have been sent in the process.

WebElement logout = driver.findElement(By.xpath("//*[@aria-label='Log out']"));
logout.click();
wait.until(new ExpectedCondition<Boolean>() {
  public Boolean apply(WebDriver d) {
    return null != d.findElement(By.className(TokenRequestWindow.STYLE_NAME_WINDOW));
  }
});
source = driver.getPageSource();
Assert.assertFalse(source.contains(LAYER_VECTOR));
Assert.assertFalse(source.contains(LAYER_RASTER));
commandCountAssert.assertEquals(2);

We will now login as a different user.


// login as other user
userName = driver.findElement(By.name("userName"));
password = driver.findElement(By.name("password"));
login = driver.findElement(By.xpath("//*[@aria-label='Log in']"));
userName.sendKeys("marino");
password.sendKeys("marino");
login.click();

The raster layer should appear again. This time there should be no vector layer and the “blabla” button should not be usable.

wait.until(new ExpectedCondition<Boolean>() {
  public Boolean apply(WebDriver d) {
    List<WebElement> elements = driver.findElements(By.xpath(LAYER_RASTER_XPATH));
    return !elements.isEmpty();
  }
});
source = driver.getPageSource();
Assert.assertFalse(source.contains(LAYER_VECTOR));
blabla = driver.findElement(By.xpath("//*[@aria-label='blabla']"));
Assert.assertTrue(blabla.getAttribute("style").contains("visibility: hidden"));

You can have a look at the full class file here.

2 Comments

  1. cj says:

    These are really good instructions. One thing the command line option to skip selenium tests is -DskipSelenium not -PskipSelenium

  2. joachim says:

    Thanks for the remark, fixed in the post.

Leave a Reply

Your email address will not be published. Required fields are marked *

question razz sad evil exclaim smile redface biggrin surprised eek confused cool lol mad twisted rolleyes wink idea arrow neutral cry mrgreen

*