Today I've been playing a little with WebDriver for testing GWT apps. First impressions: * WebDriver is awesome - can't wait to try testing more complex web-apps! * WebDriver opens up all kinds of possibilities beyond testing web-apps - for example to drive interactive demos and tutorials. * Did I mention WebDriver is awesome? :)

Preparing the Eclipse project

To get started I made a new toy project to play in. I don't use the Google Eclipse plugins for GWT dev - if you do you could create this project using the wizards instead. I develop in Eclipse, but always set up my projects with Maven, so the first step for me was to create a new maven project:

mvn archetype:create -DgroupId=com.sjl -DartifactId=webdriver

Next, create the Eclipse project so I can use eclipse to edit the maven pom...

mvn eclipse:eclipse

And then edit pom.xml in Eclipse to add the GWT, JUnit4 and WebDriver dependencies (by default maven adds JUnit 3.8, so I replace that with JUnit 4.4).

To add the GWT dependencies you also need a repository that actually has them - I use the CodeHaus gwt maven plugins during the build step, which also exposes the CodeHaus repository for fetching the dependences:

  <pluginRepositories>
    <pluginRepository>
      <id>codehaus-snapshots</id>
      <name>Codehaus plugin snapshot repository</name>
      <url>http://snapshots.repository.codehaus.org</url>
    </pluginRepository>
  </pluginRepositories> 
  ...
  <dependencies>
   <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-dev</artifactId>
      <version>2.0.4</version>
      <optional>true</optional>
   </dependency>
   <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-user</artifactId>
      <version>2.0.4</version>
      <scope>provided</scope>
   </dependency>
   ...

Next the WebDriver dependencies - I got these as transitive dependencies of selenium2:

  <dependency>
     <groupId>org.seleniumhq.selenium</groupId>
     <artifactId>selenium</artifactId>
     <version>2.0a4</version>
  </dependency>

Having edited the pom.xml file you need to rebuild the eclipse project using the eclipse maven plugin:

mvn eclipse:eclipse

This will take a little while to download the dependencies if you don't already have them - the gwt dependencies in particular are quite large. Once the command completes you need to refresh the project in Eclipse for it to reload the project classpath.

Creating a simple GWT app

I made a really simple application to play with, consisting of just an EntryPoint that displays a Label, and a PushButton which adds more Label's when clicked. I had just a handful of aims in mind for this first attempt: 1. Get a GWT app to load via WebDriver 2. Find an element which GWT has created (the first Label) 3. Interact with an element that does something (the PushButton) 4. Check that the something actually happened and that we can test for it.

The GWT code for this is extremely simple:

package com.sjl.webdriver.client;

import com.google.gwt.core.client.*;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.user.client.ui.*;

public class SimpleApp implements EntryPoint {

  public void onModuleLoad() {
    final RootPanel _root = RootPanel.get();
    _root.add(new Label("Hello"));
    _root.add(newButton());
  }

  private PushButton newButton() {
    PushButton _b = new PushButton("push me");
    _b.ensureDebugId("button");

    _b.addClickHandler(new ClickHandler(){
    public void onClick(ClickEvent event) {
    _root.add(new Label("whoop"));
      }
    });

    return _b;
  }
}

Pushing the button adds a new Label for each push - something we should be able to detect pretty easily.

My gwt module file looks like this:

 <module rename-to="webdriver">  
  <inherits name="com.google.gwt.user.User"/>
  <inherits name="com.google.gwt.user.Debug"/>
  <entry-point class="com.sjl.webdriver.client.SimpleApp" />  
 </module>

Next I made an ApplicationDriver class to wrap up the details of getting WebDriver to interact with the web app and let our tests deal with the web app in terms of a ubiquitous domain language - overkill for this simple example i'm sure, but if you want to write expressive tests for a complex web app you really need to do so at a higher level than WebDriver invocations.

The ApplicationDriver class looks like this:

package com.sjl.webdriver;

import java.util.*;
import org.openqa.selenium.*;
import org.openqa.selenium.firefox.*;

public class ApplicationDriver {

  private WebDriver driver;

  public ApplicationDriver() {
    System.setProperty("webdriver.firefox.profile", "default");
    driver = new FirefoxDriver();
    driver.get(
    "http://127.0.0.1:8888/index.html?" + 
    "gwt.codesvr=127.0.0.1:9997"); 
  }

  public void pushTheButton() { 
    WebElement _element = driver.findElement(
    By.id("gwt-debug-button"));
    _element.click();
  }

  public int countLabels() {
    List<WebElement> _elements = driver.findElements(
    By.className("gwt-Label"));
    return _elements.size();
  }

  public void quit() {
    driver.close();
  }
}

Now there are a few things worth noting here, as it took me a while to arrive at this point with something that actually worked.

First of all, notice that i'm using the FirefoxDriver implementation. I had started out with HtmlUnitDriver which works just fine for testing a compiled GWT app (provided you create it with javascript support - new HtmlUnitDriver(true) - and ignore the warnings about x-javascript) but I wanted to test during development by running OOPHM.

Stupidly I was trying to get this to work with the HtmlUnitDriver for a while, til I had a Homer Simpson moment (doh!) and realized that of course HtmlUnitDriver can't work - it doesn't have a GWT plugin :).

Secondly, the FirefoxDriver implementation starts up an instance of Firefox with a profile called "WebDriver". If you don't have a profile with that name Firefox will create one when it starts, but it won't have any of your plugins (including the GWT plugin!).

Creating a Firefox profile for WebDriver

Its easy to create a new profile (close all instances of firefox down completely then run it from the cmdline with the -profilemanager switch) - precise instructions vary by platform. I created the profile then just copied my existing profile contents to it to save installing all the plugins again. In the sample code above i've told WebDriver to use the "default" profile instead by setting a system property.

Finally, note that I'm finding the PushButton by id lookup. To make this work you have to force GWT to spit out an id for the element, and allow for the fact that GWT adds a prefix ("gwt-debug-") to the id you specify. To force GWT to produce id's for your Widget elements:

  • Inherit the Debug module in your module descriptor file (.gwt.xml)
  • Set an id on the Widget using widget.ensureDebugId("theId");

Last of all I built the testcase on top of the ApplicationDriver, starting with a test to detect the first Label, then a test to click the PushButton and check that a new Label is added. The testcase looks like this:

package com.sjl.webdriver;

import static junit.framework.Assert.*;
import org.junit.*;

public class TestSimpleApp {

  private ApplicationDriver driver;

  @Before
  public void openBrowser() {
    driver = new ApplicationDriver();
  }

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

  @Test
  public void canDetectALabel() {
    assertEquals(1, driver.countLabels());
  }

  @Test
  public void pushingTheButtonAddsALabel() {
    assertEquals(1, driver.countLabels());
    driver.pushTheButton();
    assertEquals(2, driver.countLabels());
  }

  @Test
  public void pushingTheButtonAgainAddsAnotherLabel() {
    assertEquals(1, driver.countLabels());
    driver.pushTheButton();
    assertEquals(2, driver.countLabels());
    driver.pushTheButton();
    assertEquals(3, driver.countLabels());
  }
}

That's it ... start up GWT OOPHM, run the Junit testcase, and marvel as Firefox starts up, runs the app, and shuts down again (3 times - once for each test), leaving you with a nice green bar in JUnit. I'm sure that restarting firefox between each test would be a bad idea - slooooow - in practice, I just wanted to try it to see that it worked :)

The next interesting test to play with will be testing asynchronous activity like AJAX requests. I'm hoping there'll be some nice Patterns described by the WebDriver community for writing such tests.

blog comments powered by Disqus