Turn your Selenium tests into DSL-based acceptance tests in Fitnesse

  1. Download the Fitnesse jar from http://fitnesse.org/FrontPage.FitNesseDevelopment. Move the jar to e.g. /usr/local/fitnesse.
  2. Download the Selenium Server jar from http://seleniumhq.org/download/ and move the jar to e.g. /usr/local/selenium.
  3. Start the Selenium server (will listen to port 4444 by default):
    cd /usr/local/selenium/ ; java -jar ./selenium-server.jar -log ./selenium-server.log
  4. Start the Fitnesse server (will listen to port 80 by default, but you can override this setting using the -p option):
    cd /usr/local/fitnesse/ ; java -jar ./fitnesse.jar -p 8080
  5. In a shell, create a new jar-project in e.g. ~/projects/:
    mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.mycompany.fitnesse -DartifactId=mywebtest
  6. Goto ~/projects/mywebtest and add the following dependencies to the pom.xml:
    <dependencies>  <dependency>    <groupId>org.testatoo.openqa</groupId>    <artifactId>selenium-java-client-driver</artifactId>    <version>1.0.2_20090715</version>  </dependency>
    
      <dependency>    <groupId>org.fitnesse</groupId>    <artifactId>fitnesse</artifactId>    <version>${fitnesse-version}</version>  </dependency>
    
      <dependency>    <groupId>org.fitnesse</groupId>    <artifactId>fitlibrary</artifactId>    <version>${fitnesse-version}</version>  </dependency>
    
      <dependency>    <groupId>net.sf.webtestfixtures</groupId>    <artifactId>webtestfixtures</artifactId>    <version>2.0.1.3</version>  </dependency> <dependencies>
  7. For convenience, enable jar artifact with dependencies by adding the following plugin to your pom.xml:
    <plugins>  <plugin>    <artifactId>maven-assembly-plugin</artifactId>    <version>2.2</version>    <configuration>      <descriptorRefs>        <descriptorRef>jar-with-dependencies</descriptorRef>        </descriptorRefs>       </configuration>    <executions>      <execution>        <id>make-assembly</id>         <phase>package</phase>         <goals>          <goal>single</goal>         </goals>      </execution>    </executions>  </plugin></plugins>
  8. (Optional) For Eclipse users, create project file for mywebtest:
    mvn eclipse:eclipse
  9. Create your WebTest fixture class, using your favorite editor (that is, Eclipse :-)):
    package com.mycompany.fitnesse;   import com.neuri.webfixture.WebTest; public class MyWebTest extends WebTest {}
  10. Build the jar artifact and copy the jar to /usr/local/fitnesse/
    mvn install ; cp target/mywebtest-1.0-SNAPSHOT-jar-with-dependencies.jar /usr/local/fitnesse
  11. Goto http://localhost:8080 and click the “Edit” link. Add the text MyTest and click save.
  12. Click on the question mark next to your new test, add the following text and then save (the backslash below denotes only that the line continues on the next line and should not be part in the actual test specification):
    !define TEST_RUNNER {fitlibrary.suite.FitLibraryServer}!path mywebtest-1.0-SNAPSHOT-jar-with-dependencies.jar!path lib/*.jar!|com.mycompany.fitnesse.MyWebTest||Start Browser |firefox|With Selenium Console On|localhost|At     Port|4444|And Scripts At|http://www.amazon.com/||user opens URL | / || user adds the book with title | Fit for Developing Software:     Framework for Integrated Tests | to the shopping cart || the shopping cart should contain a book with title | Fit for     Developing Software: Framework for Integrated Tests || the shopping cart should contain | 1 | item |

    The test specification

  13. Click on properties and change the page property from Normal to Test. Then click on “Save properties”.
  14. Run the Fitnesse test by clicking on the Test button located at localhost:8080/MyTest. You will get exceptions like (see image):No test methods implemented
  15. Add the missing methods to the class MyWebTest:
    public class MyWebTest extends WebTest {   public void userAddsTheBookWithTitleToTheShoppingCart(String title) {  }   public boolean theShoppingCartShouldContainABookWithTitle(String title) {    return false;  }   public boolean theShoppingCartShouldContainItem(int numOfItems) {    return false;  }}
  16. Build the jar artifact again and copy the jar to /usr/local/fitnesse/:
    mvn install ; cp target/mywebtest-1.0-SNAPSHOT-jar-with-dependencies.jar /usr/local/fitnesse
  17. Run the Fitnesse test again and see the difference.After stub methods have been implemented
  18.  We need now to implement the test methods, using the the primitive operations offered by the base class WebTest:
    public void userAddsTheBookWithTitleToTheShoppingCart(String title) {
      userOpensURL("/");  waitSecondsForElementToAppear(5000, "navSearchDropdown");  userSelectsFrom("label=Books", "searchDropdownBox");  userTypesIntoField(title, "twotabsearchtextbox");  instance.submit("site-search");  pageReloadsInLessThanSeconds("30");  userClicksOn(title);  waitSecondsForElementToAppear(30000, "bb_atc_button");  userClicksOn("bb_atc_button");}
    
    public boolean theShoppingCartShouldContainABookWithTitle(String title) {  userClicksOn("Cart");  waitSecondsForElementToAppear(5000, "cartViewForm");  return pageContainsText(title);}
    
    public boolean theShoppingCartShouldContainItem(int numOfItems) {  userClicksOn("Cart");  waitSecondsForElementToAppear(5000, "cartViewForm");  String actualValue = this.instance.getValue("quantity.1");   return numOfItems == Integer.parseInt(actualValue);}
  19. Build the jar artifact again and copy the jar to /usr/local/fitnesse/:
    mvn install ; cp target/mywebtest-1.0-SNAPSHOT-jar-with-dependencies.jar /usr/local/fitnesse
  20. Run again and the test should be all green 🙂Test methods have been implemented

One detail I have not mentioned is how-to know which elements to use in the test. I used Firebug in combination with Selenium IDE to get hints of what elements I could use. In this particular example, the Selenium IDE produced the following test class:

     @Test     public void testAmazon() throws Exception {       selenium.open("/");       selenium.select("searchDropdownBox", "label=Books");       selenium.type("twotabsearchtextbox", "Fit for Developing Software:           Framework for Integrated Tests");       selenium.click("//input[@type='image']");       selenium.waitForPageToLoad("30000");       selenium.click("link=Fit for Developing Software: Framework for           Integrated Tests");       selenium.waitForPageToLoad("30000");

       for (int second = 0;; second++) {         if (second >= 60) fail("timeout");         try { if (selenium.isElementPresent("bb_atc_button")) break; }          catch (Exception e) {}         Thread.sleep(1000);       }

       selenium.click("bb_atc_button");       selenium.waitForPageToLoad("30000");       selenium.click("link=Edit your Cart");       selenium.waitForPageToLoad("30000");       verifyTrue(selenium.isTextPresent("Fit for Developing Software:           Framework for Integrated Tests"));       verifyEquals("1", selenium.getValue("quantity.1"));     }

For most of the operations, I was able to use corresponding WebTest operations that are of a slightly higher abstraction level. However, to submit the search for books, I used the Selenium instance directly, since the form had a more readable id (“site-search”). In order to get hold of the Selenium instance, I needed MyWebTest to inherit from WebTest (painful, since I prefer composition :-))

As you may have noticed, I needed to add wait statements. Thus, the problem with brittle Selenium tests is still there. However, I believe Fitnesse test is more readable compared to the Selenium test. In addition, the tests can be modified by a non-programmer and understandable by business people. Thus, tests like these can bridge the gap between customers, business people, developers and testers. Once you have developed your own DSL (Domain-Specific Language), you’re ready to write acceptance test first together with the stakeholder/product owner. That is, you’re able specify executable specifications before the actual functionality is in place, which is very attractive to a test-driven addict  as myself :-).

What remains is to include the Fitnesse tests in your the CI server. But that is another blog entry to be written. 🙂

7 responses on “Turn your Selenium tests into DSL-based acceptance tests in Fitnesse

  1. Hi Magnus, I’m also incrementally extracting a DSL from the acceptance test suite.

    Would like to ear more about the DSL that is emerging from your test suite. Here is mine.

    I.e. in my test suite sometime I also use the Selenium instance directly, so 3 classes initially emerged:
    => Application Verifications: with the assertions like theShoppingCartShouldContainItem
    => User: with the actions user performs as userAddsTheBookWithTitleToTheShoppingCart and the data user needs to remember (as a system generated password)
    => Application Urls: with urls used when I use the Selenium instance directly
    => Application Ids: with html ids useful when I use the Selenium instance directly

  2. Hi, we’re in the beginning of creating our DSL, so hopefully I have more information later on 🙂 However, we’re using Selenium-based, or procedural, tests for web-based user stories and other, declarative, test fixtures for verifying business rules.

  3. It’s a great post, you really are a good writer! I’m so glad someone like you have the time, efforts and dedication writing, for this kind of article… Helpful, And Useful.. Very nice post!

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.