Monday, November 16, 2015

Integrate Cucumber with WebDriver: BDD in automation

In this post, I show how Cucumber can be integrated with Selenium WebDriver for bringing BDD in end-to-end testing. As a system under testing, I choose the BBC website and test basic navigation on BBC news pages. The code is available on github.

Background

Make no mistake, cucumber's main purpose is not to be a testing tool, but to enforce collaboration between scrum teem members. The main objective is to ensure that all the team members have the same understanding of the requirements of the feature they have to develop and test. Business analysts that, together with the product owner, have the basic idea of the feature requirements, introduce it to the team and the team as a whole finalizes the requirements in the form of Cucumber Scenarios. These scenarios are written in Gherkin, a DSL very close to natural language. Scenarios can be created in different level of granularity, e.g. for unit, integration or end-to-end tests. Cucumber initiates the tests that are initially failing, as the feature has not yet been developed and then development starts. Scenarios do not only guide the testing, but also consist a live documentation of the feature. 

In this post, I focus on end-to-end tests (frequently called 'automation'). These are typically written using the Selenium WebDriver framework. As it is well known, end-to-end tests have a high maintenance cost, e.g. when the UI changes, tests might fail, as some web elements may no longer be present or may have been moved. The work you need to fix them highly depends on the presence or absence in/from your test code of design patterns such as the Page Object and the Loadable Component patterns. In this post, I show the benefit for using them.

Setup

As a build and dependency management tool, I use maven. The pom file follows.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ytheohar</groupId>
<artifactId>bbc-webdriver-cucumber</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<cucumber.version>1.2.3</cucumber.version>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>2.48.2</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-java8</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
view raw pom.xml hosted with ❤ by GitHub
The Hamcrest library is optional, you can integrate Cucumber with Selenium WebDriver without it, but it is nice to have it, as it provides an elegant way to write assertions. Also I use 'cucumber-java8', as this allows for steps to be defined in lamdas instead of methods and this reduces the boilerplate.

BBC News Feature

Let's say we are in the team that develops/maintains the BBC website and in particular the news part. The feature we need to develop is that the user should be able to navigate from the home page to the news page, then select a news category, e.g. 'Science', then select a (let's say the first for simplicity) video page from the 'Watch/Listen' section and then share this video page on Facebook. As the feature is implemented by the real BBC team, you can try it. The sequence of the links could be the following, but always keep in mind that the Cucumber scenarios should be written before the development: 
  1. BBC Home
  2. BBC News
  3. BBC News Science category
  4. BBC Science Video Page
  5. Facebook referral page
The Cucumber feature file follows.

Feature: Navigate to video page and share it on facebook
The user should be able to navigate through news categories, select a video page and share it on facebook
Scenario: Navigate to 'News' page by clicking on 'Latest news'
Given the user is landed at the home page
When the user clicks on 'Latest news'
Then the 'News' page loads
Scenario: Navigate to 'News' page by clicking on 'News' on the navigation bar
Given the user is landed at the home page
When the user clicks 'News' on the navigation bar
Then the 'News' page loads
Scenario Outline: Navigate to a Video Page and share it on Facebook
Given the user is landed at the home page
When the user clicks 'News' on the navigation bar
And the user clicks '<category>' on the inner news navigation bar
And the user clicks on the first video on the right hand 'Watch/Listen' section
And the user clicks on the facebook share icon
Then the facebook page loads having a url that includes the video page url
Examples:
| category |
| UK |
| Business |
| Politics |
| Tech |
| Science |
| Health |
| Education |
| Entertainment & Arts |
The first two scenarios test that the user can navigate to the BBC News page by clicking either on the 'Latest news' link of the homepage or on the 'News' link on the navigation bar at the top of the home page.

The third scenario, which more precisely is a 'scenario outline' or a set of scenarios if you wish, tests the main feature which is to navigate from home page to news page, then to a news category page, then to the first video page of the category and finally to share it on facebook.

Gherkin is very easy to follow, shortly Feature, Scenario, Given, When, Then, And are cucumber reserved keywords, while the rest is English. Each sentence starting with Given, When, Then defines a step. For more info about Gherkin, steps and scenario outline, you can read the post BDD with Cucumber vs Spock. The main thing to keep is a cucumber scenario is the analog of a junit/testng test method, while a scenario outline is the analog of a set of calls to the same testng test method that takes an input whose value is provided by a testng data provider. The cucumber analog for the data provider is the Examples table. In short, category is the input, and the scenario outline tests that the feature works for any BBC news category appearing in the Examples table.

The Page Object Pattern

The main goal of the Page object pattern is to encapsulate details related to a page into a class object and expose only methods relevant with the functionality this page offers. Take for instance the news category page, the locator of the first video page link in the 'Watch/Listen' section is an internal detail that should be hidden from tests. The news category page object only needs to provide a 'clickFirstVideo' method and that's the only thing test code needs to be aware of. 

The benefit is that when the UI changes and the locator is no longer valid, you will have to fix it on a single place, on the related page object. Experience shows that if you don't use the page object pattern, the end-to-end code maintenance cost will explode sooner or later.

The Loadable Component Pattern

While the page object pattern hides the details behind functionality offered by the page, the Loadable Component pattern hides the details behind the navigation to the page object. To apply this, WebDriver offers an abstract LoadableComponent class which your page object class has to extend. It has two abstract methods 'isLoaded' and 'load' that the page object class has to implement and a concrete method 'get' that calls 'isLoaded' to check if the page is already loaded and if not it calls the 'load' method and then the 'isLoaded' to verify it is finally loaded. The idea is to pass to the constructor of a page object class a reference to the parent page object and in the 'load' method of the child class to call first the 'get' method on the parent object (to load the parent page) and then call some functionality method on the parent object that will load the child page.

The implementation of the NewsCategoryPage class that implements both the page object and the loadable component patterns follows.

public class NewsCategoryPage extends LoadableComponent<NewsCategoryPage> {
private final WebDriver driver;
private final LoadableComponent<NewsPage> parent;
private final String baseUrl;
private final Category cat;
@FindBy(id = "comp-index-title")
private WebElement titleElem;
@FindBy(xpath = "//div[contains(@class,'column--secondary')]//div[@class='condor-item faux-block-link'][1]//a[contains(@class,'title-link')]")
private WebElement video;
public NewsCategoryPage(WebDriver driver, LoadableComponent<NewsPage> parent, Category cat) {
this.driver = driver;
this.parent = parent;
this.cat = cat;
baseUrl = "http://www.bbc.co.uk/news" + cat.getPath();
PageFactory.initElements(driver, this);
}
@Override
protected void isLoaded() throws Error {
String url = driver.getCurrentUrl();
String message = "News content page has not been loaded correctly";
assertThat(message, url, equalTo(baseUrl));
String actualTitle = titleElem.getText();
assertThat(message, actualTitle, equalTo(cat.getTitle()));
}
@Override
protected void load() {
parent.get().selectCategory(cat);
}
/**
* Clicks on the first video page link on the right hand 'Watch/Listen'
* section
*
* @return the video page object
*/
public VideoPage clickFirstVideo() {
String path = video.getAttribute("href");
video.click();
return new VideoPage(driver, path);
}
/**
* Asserts this page is correctly loaded
*/
public void shouldBeLoaded() {
isLoaded();
}
}
Notice the implementation of the 'load' method. It first calls 'parent.get()' to load the news page and then calls the 'selectCategory' method on the news page object to load the news category page.

It should be stressed that the loadable component pattern does not necessarily tight a page object class with a single navigation path. Take for instance the News page, you have 2 ways to navigate to it from the BBC home page by clicking on either 'Latest news' link on the main section, or on the 'News' link on the navigation bar. The relevant part of the news page object class follows (you can find the whole class code on github).

public class NewsPage extends LoadableComponent<NewsPage> {
private final WebDriver driver;
private final static String baseUrl = "http://www.bbc.co.uk/news";
private final LoadableComponent<?> parent;
...
public NewsPage(WebDriver driver, LoadableComponent<?> parent) {
this.driver = driver;
this.parent = parent;
PageFactory.initElements(driver, this);
}
@Override
protected void isLoaded() throws Error {
String url = driver.getCurrentUrl();
assertThat("Home page has not been loaded correctly", url, equalTo(baseUrl));
}
@Override
protected void load() {
parent.get();
if (parent instanceof HomePage) {
((HomePage) parent).clickLatestNews();
} else if (parent instanceof NavigationBarPage) {
((NavigationBarPage) parent).selectNews();
}
}
...
}
view raw NewsPage.java hosted with ❤ by GitHub
Steps Implementation

The Steps implementation follows.

public class BBCNewsSteps implements En {
private WebDriver driver;
private HomePage homePage;
private NewsPage newsPage;
private NewsCategoryPage newsCategoryPage;
private VideoPage videoPage;
private FBPage fbPage;
@Before
public void setup() {
driver = new FirefoxDriver();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
}
@After
public void tearDown() {
driver.quit();
}
public BBCNewsSteps() {
Given("^the user is landed at the home page$", () -> {
homePage = new HomePage(driver);
homePage.get();
});
When("^the user clicks 'News' on the navigation bar$", () -> {
NavigationBarPage navbar = new NavigationBarPage(driver, homePage);
newsPage = new NewsPage(driver, navbar);
newsPage.get();
});
When("^the user clicks '(.*)' on the inner news navigation bar$",
(String categoryLinkTitle) -> {
Category category = Category.of(categoryLinkTitle);
newsCategoryPage = new NewsCategoryPage(driver, newsPage,
category);
newsCategoryPage.get();
});
When("^the user clicks on the last video on the right hand 'Watch/Listen' section$",
() -> {
videoPage = newsCategoryPage.clickFirstVideo();
});
When("^the user clicks on the facebook share icon$", () -> {
fbPage = videoPage.shareOnFacebook();
});
Then("^the facebook page loads having a url that includes the video page url$",
() -> {
fbPage.shouldBeLoaded();
});
When("^the user clicks on 'Latest news'$", () -> {
newsPage = new NewsPage(driver, homePage);
newsPage.get();
});
Then("^the 'News' page loads$", () -> {
newsPage.shouldBeLoaded();
});
}
}
You can run the scenarios either on eclipse by right clicking on the BBCNewsTest.java -> Run As -> Junit test, or with maven: mvn clean test

Further Improvement

A nice observation for the loadable component pattern is that that if the page object class implements it and you call the 'get' method on it, you implicitly run assertions that verify the page has been 'correctly' loaded, where 'correctly' is defined in the 'isLoaded' method implementation. Despite its name that starts with 'is' this method does not return a boolean value. Instead, its return type is void, while it 'throws Error'. This typically contains all the assertions needed to verify it was loaded properly.

To understand this, recall how get method is implemented in the LoadableComponent class.

public abstract class LoadableComponent<T extends LoadableComponent<T>> {
public T get() {
try {
isLoaded();
return (T) this;
} catch (Error e) {
load();
}
isLoaded();
return (T) this;
}
...
}
It should now be obvious that the second scenario is redundant. If the news page does not load when you click on 'News' link on the home page navigation bar, then the Step "When the user clicks 'News' on the navigation bar", which is shared in the second scenario and the scenatrio outline, will fail, due to the assertions in the NewsCategoryPage 'isLoaded' method. Therefore the second scenario can be safely omitted.

No comments:

Post a Comment