Saturday, January 7, 2012

EasyB, Thucydides and Selenium 2 - another BDD example

Today I'll present you quick example of usage for EasyB - BDD framework for Java :), accompanied by Thucydides - amazing tool for ATDD build on top of Selenium 2.

Suppose that you want to leave something for the humanity, after many sleepless nights you finally discover that it will be a movie database, even more: Internet Movie Database. Now, my Friend, you have two ways: you can start writing this service immediately, forgetting about the eating and sleeping ... When you'll find yourself abandoned by all your Friends, and lost completely in the endless code you wrote, without helpful hand from the tests you were too busy (or even worse, too proud) to wrote ... You'll find the path to enlightenment - second way. Slower, but built on rock solid foundations - ATDD, BDD, and maybe even few more acronyms ;)

Before you yell: "Teach me this way!" few words of explanation. I'm not the Master Yoda. I'm still learning, as you do, and will whole my life, but maybe some parts of my daily exercises will help you somehow ...

Back to the example :) - What we do first, is defining the core functionality required by our service, and describing it in EasyB syntax. Note that in this post we will focus on testing service only. Instead of writing the service, we will use existing Internet Movie Database, and verify if it matches our expectations:

scenario "User can search for movie title",
{
given "the user opens application"
when "the user performs search for movie title existing in database"
then "the system presents at least one result"
}

What you see is one of the use cases I assumed for the service. How can we bring it to life now? For example, using Apache Maven, if you decide to choose it as a build system for your project, as I did.

What you need is adding to your pom.xml file few lines defining the EasyB plugin (under build/plugins section):

<plugin>
<groupId>org.easyb</groupId>
<artifactId>maven-easyb-plugin</artifactId>
<version>1.4</version>
<configuration>
<storyType>html</storyType>
<storyReport>${basedir}/target/easyb/easyb.html</storyReport>
<easybTestDirectory>${basedir}/src/test/stories</easybTestDirectory>
</configuration>
</plugin>

Now we may try to run this scenario and see what EasyB will tell us. We can do it for ex. with:
mvn clean test easyb:test
Report generated by EasyB looks like this one:


Interesting, but the real fun begins with Thucydides usage. First we have to get back to Apache Maven configuration, and change the report type generated by EasyB from HTML to the default one (XML), otherwise there will be nothing to process for Thucydides.

<plugin>
<groupId>org.easyb</groupId>
<artifactId>maven-easyb-plugin</artifactId>
<version>1.4</version>
<configuration>
<easybTestDirectory>${basedir}/src/test/stories</easybTestDirectory>
</configuration>
</plugin>
<plugin>
<groupId>net.thucydides.maven.plugins</groupId>
<artifactId>maven-thucydides-plugin</artifactId>
<version>0.6.1</version>
</plugin>

Now we may try to run our scenario in following way:
mvn clean test easyb:test thucydides:aggregate
and start admiring report generated by Thucydides, which looks like this one:

Summary Page
Features Summary
Stories Summary
Story Details
At this point you probably think - Gosh, looks nice, but how it can be useful for me, as the developer?!

Now we get to the clue :) - as you probably noticed, some steps defined in the scenario are in pending state. This is the moment when the Business Management Team role ends ;), and starts our own :), because we, as the Developers, have to implement the tests standing behind the scenarios. :)

You may also wonder if Thucydides role here is beautifying the reports only - of course not! - it shows its potential when we start to implement tests :).

Let's return to our scenario description:

using "thucydides"
import com.blogspot.vardlokkur.requirements.Application.MovieSearch.SearchForTitles
import com.blogspot.vardlokkur.steps.UserSteps
import com.blogspot.vardlokkur.steps.SystemSteps
thucydides.uses_default_base_url "http://www.imdb.com/"
thucydides.uses_steps_from UserSteps
thucydides.uses_steps_from SystemSteps
thucydides.tests_story SearchForTitles
scenario "User can search for movie title",
{
given "the user opens application",
{
system.presentsWelcomePage();
}
when "the user performs search for movie title existing in database",
{
user.performsSearchForExistingTitle();
}
then "the system presents at least one result",
{
system.presentsAtLeastOneResult();
}
}

First of all, we group all features and use cases of our application in 3 level structure - Application - Feature - Use Cases (Scenarios):

package com.blogspot.vardlokkur.requirements;
import net.thucydides.core.annotations.Feature;
/**
* Groups together the features desired for the application.
*
* @author warlock
*/
@SuppressWarnings("PMD.AtLeastOneConstructor")
public class Application {
/**
* Movie Search is the main feature of our application.
*/
@Feature
public class MovieSearch {
/**
* Title search.
*/
public class SearchForTitles {
}
}
}

Now we implement the steps defined in scenario for the system:

package com.blogspot.vardlokkur.steps;
import net.thucydides.core.annotations.Step;
import net.thucydides.core.pages.Pages;
import net.thucydides.core.steps.ScenarioSteps;
import com.blogspot.vardlokkur.pages.ResultsPage;
import com.blogspot.vardlokkur.pages.WelcomePage;
/**
* Defines steps taken by system during the application usage.
*
* @author warlock
*/
public class SystemSteps extends ScenarioSteps {
/**
* Constructs new instance.
*
* @param pages
*/
public SystemSteps(final Pages pages) {
super(pages);
}
@Step("the system presents at least one result")
public void presentsAtLeastOneResult() {
final ResultsPage page = getPages().currentPageAt(ResultsPage.class);
page.containsAtLeastOneResult("Sherlock Holmes");
}
@Step("the system presents welcome page")
public void presentsWelcomePage() {
final WelcomePage page = getPages().get(WelcomePage.class);
page.open();
}
}

and for the user:

package com.blogspot.vardlokkur.steps;
import net.thucydides.core.annotations.Step;
import net.thucydides.core.pages.Pages;
import net.thucydides.core.steps.ScenarioSteps;
import com.blogspot.vardlokkur.pages.WelcomePage;
/**
* Defines steps taken by users during the application usage.
*
* @author warlock
*/
public class UserSteps extends ScenarioSteps {
/**
* Constructs new instance.
*
* @param pages
*/
public UserSteps(final Pages pages) {
super(pages);
}
@Step("the user performs search for movie title existing in database")
public void performsSearchForExistingTitle() {
final WelcomePage page = getPages().currentPageAt(WelcomePage.class);
page.searchForTitle("Sherlock Holmes");
}
}
view raw UserSteps.java hosted with ❤ by GitHub

And finally the code reflecting pages of the service, for welcome page:

package com.blogspot.vardlokkur.pages;
import net.thucydides.core.annotations.DefaultUrl;
import net.thucydides.core.pages.PageObject;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* Reflects welcome page functionality.
*
* @author warlock
*/
@DefaultUrl("http://www.imdb.com/")
public class WelcomePage extends PageObject {
@FindBy(id = "navbar-query")
private transient WebElement queryString;
@FindBy(id = "quicksearch")
private transient WebElement searchType;
@FindBy(id = "navbar-submit-button")
private transient WebElement submitButton;
/**
* @see PageObject#PageObject(WebDriver)
*/
public WelcomePage(final WebDriver driver) {
super(driver);
}
/**
* @see PageObject#PageObject(WebDriver, int)
*/
public WelcomePage(final WebDriver driver, final int ajaxTimeout) {
super(driver, ajaxTimeout);
}
/**
* Performs search for the title.
*
* @param queryString query string
*/
public void searchForTitle(final String queryString) {
selectFromDropdown(searchType, "Titles");
enter(queryString).into(this.queryString);
submitButton.click();
}
}

and search results:

package com.blogspot.vardlokkur.pages;
import net.thucydides.core.annotations.At;
import net.thucydides.core.annotations.DefaultUrl;
import net.thucydides.core.pages.PageObject;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
/**
* Reflects results page functionality.
*
* @author warlock
*/
@At("#HOST/find.*")
@DefaultUrl("http://www.imdb.com/find.*")
public class ResultsPage extends PageObject {
/**
* @see PageObject#PageObject(WebDriver)
*/
public ResultsPage(final WebDriver driver) {
super(driver);
}
/**
* @see PageObject#PageObject(WebDriver, int)
*/
public ResultsPage(final WebDriver driver, final int ajaxTimeout) {
super(driver, ajaxTimeout);
}
/**
* Verifies if there is at least one result for given query string.
*
* @param queryString query string
*/
public void containsAtLeastOneResult(final String queryString) {
shouldBeVisible(By
.xpath("//table//a[contains(translate(text(), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'), '"
+ queryString.toUpperCase() + "')]"));
}
}

After implementing the code standing behind our scenario, we can run it again and compare the results with expectations:

Summary Page
Features Summary
Stories Summary
Story Details
Thucydides by default creates screenshots of tested application on each UI change (it can be changed to document step failures only), which you can see below:

Welcome page opened (Given)

Title search beginning (When)

UI change - Search Type selected

UI change - search query entered

UI change - form submitted

Search results verification (Then)
Quick summary at the end ;) - EasyB looks like the great tool for BDD, even more attractive when accompanied by Thucydides and Selenium 2. The example above is very quick and simple, but maybe interesting for you, if you are still reading my post ;)

Few lectures for the dessert: