Page Object pattern with Selenium WebDriver and Cucumber Java 8 (lambda support) using PicoContainer

While Cucumber is de facto the World standard tool for behavior driven development, Selenium and WebDriver in turn are the World standard for browser autonomous interaction. Hence it is quite a usual case when people combine these two frameworks in order to gain the maximum effect when they implement BDD tests for Web or Mobile UI.

The common question (not necessarily related to Cucumber) is how we supply a WebDriver object to each particular test from our automated test set. Cucumber specific here is that while the tool supports JUnit and TestNG frameworks, it still does not recommend to use them to set up and tear down your state.

Another Cucumber-specific issue is that Cucumber controls the scenario execution and your step definitions might be spread over different classes. So there is neither no obvious joint that can be used to pass WebDriver from one step to another nor the obvious way to hold your WebDriver object persistent within the entire scenario and recreate it for another one.

The main concept that Cucumber supports in order to fulfill mentioned issues is called Dependency Injection. The latter means that there is the logic hidden from one who codes the class that takes care of binding the fields of a class to the particular objects (instances). That "hidden logic" is implemented in so called DI frameworks and the DI framework that Cucumber recommends to use is called PicoContainer.

Here in the post we’ll see how to deal with parameterized Cucumber scenario that is running using cucumber-java8 library. Another valuable point of this article is that we’re also be using Page Object approach where the pages will also be instantiated with PicoContainer. At the end we’ll discuss some downside of using PicoContainer with Cucumber.

Let’s now look at everything in more details.

Example description and dependencies versions

In our example we’re creating a test that opens this site and watches "All Posts" page for given amount of time. Let’s look at what we have as our starting point.

Feature files

Here is the feature file with the scenario that we’re going to implement in step definition classes:

Feature: Stare

Scenario Outline: Stare at the page
  Given Open page https://webelement.click/en/welcome
  Then Go to All Posts
  And Stare for <time> seconds

Examples:
| time           |
| 5              |
| 10             |

We also have another feature file. We’re not going to run that feature test but we need it in order to demonstrate some aspects of how PicoContainer works with Cucumber so that we can implement the effective design. We’ll discuss those aspects a bit later. Now let’s just look at that "useless" feature file:

Feature: Useless

  Scenario: Useless scenario
    Given Whatever

Page objects

The same approach we use for our page objects. Here is the "useful" page object:

package click.webelement.cucumber.pages;

import click.webelement.cucumber.selenium.LazyWebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

public class HomePage {

    @FindBy(linkText = "All Posts")
    WebElement allPosts;

    public HomePage(LazyWebDriver driver){
        System.out.println("Instantiating HomePage object.");
        PageFactory.initElements(driver, this);
    }

    public void goToAllPosts(){
        System.out.println("Clicking All Posts..");
        allPosts.click();
        System.out.println("Done.");
    }

}

And here is the "useless" one:

package click.webelement.cucumber.pages;

public class UselessHomePage {

    public UselessHomePage(){
        System.out.println("Instantiating useless home page..");
    }

    public String whatever(){
        return "Whatever..";
    }

}

Having all these things ready we can move on to the feature steps implementation. But before let’s quickly look at the maven dependencies which will be used in the example.

You are probably wondering what LazyWebDriver is in our "useful" page object. This is our custom class that we’ll look at later. Such approach is used to make your code working effectively with PicoContainer and Cucumber.

Maven dependencies

To successfully run the example you will need the only three dependencies in your pom.xml. They are the Cucumber supporting Java 8, PicoContainer and Selenium.

<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>3.141.59</version>
</dependency>
<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java8</artifactId>
    <version>6.1.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-picocontainer</artifactId>
    <version>6.1.1</version>
    <scope>test</scope>
</dependency>

In my example I’m also planning to use GeckoDriver and FireFox web browser. But this does not really mater so you are free to choose any of your favorite browsers and WebDrivers.

Implementing step definitions

Since we’re using cucumber-java8 there is a special way to implement your steps definition. Your steps class have to implement io.cucumber.java8.En interface and your steps are to be described within the class constructor.

Here is the code of the steps definitions for our "useful" scenario. Let’s take a look and then discuss.

package click.webelement.cucumber.steps.definitions;

import click.webelement.cucumber.pages.HomePage;
import click.webelement.cucumber.selenium.LazyWebDriver;
import io.cucumber.java8.En;

public class DemonstrateInjectionPrimary implements En {

    HomePage homePage;
    LazyWebDriver driver;

    public DemonstrateInjectionPrimary(HomePage homePage, LazyWebDriver driver){
        this.homePage = homePage;
        this.driver = driver;
        Given("Open page {}", (String url) -> {
            System.out.println("Opening url: " + url);
            driver.get(url);
        });
        Then("Go to All Posts", ()->{
            homePage.goToAllPosts();
        });
        And("Stare for {int} seconds", (Integer seconds) -> {
            System.out.println("Staring at the page for "
                    + seconds.toString()
                    + " seconds..");
            Thread.sleep(seconds * 1000);
        });
    }

}

Our steps are to operate with WebDriver and HomePage page object, hence we need to let PicoContainer know that it has to inject them through the class constructor.

As we will see a bit later, class LazyWebDriver implements WebDriver interface so that our steps work with it as with any other driver.

There is also a "useless" pair for our step definition where we implement the step for "useless" feature file. Here it is:

package click.webelement.cucumber.steps.definitions;

import click.webelement.cucumber.pages.UselessHomePage;
import io.cucumber.java8.En;


public class DemonstrateInjectionSecondary implements En {

    UselessHomePage uselessHomePage;

    public DemonstrateInjectionSecondary(UselessHomePage uselessHomePage){
        this.uselessHomePage = uselessHomePage;
        Given("Whatever", () -> {
            System.out.println(uselessHomePage.whatever());
        });
    }
}

Notice that code of "useful" and "useless" scenarios is not intersecting. Code of "useful" scenario does not use the state implying for "useless" one. This will matter later on.

How do I tell Cucumber and PicoContainer how to initialize my classes for field injection

Unlike other DI frameworks Cucumber supports PicoContainer out of the box. So you only need to add the dependency to your pom.xml and it will automatically search for the classes in your classpath. It will look for default constructor, otherwise (if a constructor takes some objects) it will look for corresponding classes with default constructors, etc.

Consider code efficiency when use PicoContainer

When PicoContainer is starting, it tries to instantiate all the classes it knows about associated with all the feature files in your classpath, even those you are not currently running. This might be a problem when creating an instance is a heavy process. Creating a WebDriver object is exactly such kind of process since we are binding to a driver server, the browser is getting opened, etc.

This is why we have "useless" scenario here. When everything will be ready for execution you will see that even when you run "stare" feature you can see the console footprint of the "useless" objects that proves they are being created even when you do not need them.

The implementation of LazyWebDriver addresses performance issues since it provides the way for lazy driver initialization. Let’s look at the implementation and then highlight important points:

package click.webelement.cucumber.selenium;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.picocontainer.Disposable;

import java.util.List;
import java.util.Set;

public class LazyWebDriver implements WebDriver, Disposable {

    private WebDriver delegate = null;

    private WebDriver getDelegate() {
        if (delegate == null) {
            System.setProperty("webdriver.gecko.driver", "/path_to_webdriver/geckodriver");
            delegate = new FirefoxDriver();
        }
        return delegate;
    }

    @Override
    public void get(String url) {
        getDelegate().get(url);
    }

    @Override
    public String getCurrentUrl() {
        return getDelegate().getCurrentUrl();
    }

    @Override
    public String getTitle() {
        return getDelegate().getTitle();
    }

    @Override
    public List<WebElement> findElements(By by) {
        return getDelegate().findElements(by);
    }

    @Override
    public WebElement findElement(By by) {
        return getDelegate().findElement(by);
    }

    @Override
    public String getPageSource() {
        return getDelegate().getPageSource();
    }

    @Override
    public void close() {
        getDelegate().close();
    }

    @Override
    public void quit() {
        getDelegate().quit();
    }

    @Override
    public Set<String> getWindowHandles() {
        return getDelegate().getWindowHandles();
    }

    @Override
    public String getWindowHandle() {
        return getDelegate().getWindowHandle();
    }

    @Override
    public TargetLocator switchTo() {
        return getDelegate().switchTo();
    }

    @Override
    public Navigation navigate() {
        return getDelegate().navigate();
    }

    @Override
    public Options manage() {
        return getDelegate().manage();
    }

    @Override
    public void dispose() {
        System.out.println("Killing WebDriver");
        if(delegate != null){
            delegate.quit();
        }
    }
}

Long enough but all IDEs allow to generate delegation so that you would not need to write everything by hands. So, when PicoContainer encounters the field of LazyWebDriver type it instantiates it using default constructor. At that point nothing really happens except of allocating the space for LazyWebDriver in memory. No real WebDriver is created unless your code is calling any of WebDriver methods. When your step definition calls driver.get(…​) this part is executed:

private WebDriver getDelegate() {
    if (delegate == null) {
        System.setProperty("webdriver.gecko.driver", "/path_to_webdriver/geckodriver");
        delegate = new FirefoxDriver();
    }
    return delegate;
}

At this point the real driver is created. You can add the logic to decide which driver exactly has to be created (Chrome, Opera, IE, etc.). Also you need to amend the path to your driver executable. After we have called any of driver methods for the first time all other calls will be dispatched to already created driver.

Another thing that is important to know is that such classes should implement org.picocontainer.Disposable interface. It is called by the PicoContainer when it is being disposed itself. This is the good place where you can quit your WebDriver instance:

@Override
public void dispose() {
    System.out.println("Killing WebDriver");
    if(delegate != null){
        delegate.quit();
    }
}

The chain is the following: when scenario is finished Cucumber calls stop() method of ObjectFactory. It delegates the call to underlying implementation that invokes stop() and dispose() against the container. When the container is being disposed it iterates over created objects which meet the contract described in Disposable interfaces and invokes dispose method of those objects.

Run everything

So by the moment you should have two feature files. Useful and useless one. Let’s run the useful feature file and watch the console log. It should look like this:

Instantiating HomePage object.
Instantiating useless home page..
Opening url: https://webelement.click/en/welcome
Clicking All Posts..
Done.
Staring at the page for 5 seconds..
Killing WebDriver
Instantiating HomePage object.
Instantiating useless home page..
Opening url: https://webelement.click/en/welcome
Clicking All Posts..
Done.
Staring at the page for 10 seconds..
Killing WebDriver

2 Scenarios (2 passed)
6 Steps (6 passed)
0m34.620s

Apart from the fact Selenium has successfully accomplished the steps, the log shows that the objects associated with the feature file left not executed have also been instantiated.

The downside

You probably noticed (especially if you have some experience with DI concept) that we didn’t use the injections like:

MyInterface field = new MyInterfaceImpl();

This is because PicoContainer integration with Cucumber does not allow to introduce such mappings easily out of the box. In my next post about #Cucumber I’ll show you another example where we’ll implement Page Object pattern with Guice DI framewrok.

So, this is it. Here we have learned the general concept of how to create, share and dispose objects with PicoContainer within the Cucumber scenario scope in the example of Selenium and Page Object pattern.

If you still have the questions please send them to me using this form. I will amend the article according to your feedback.