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.

There is an article (that you can also find on the official Cucumber site) about how to use PicoContainer with Cucumber steps definitions. The problem is that the approach it explains (very well by the way) is relevant to a "regular" Cucumber. The idea is that you are using constructors in your steps definition classes. Those constructors have to take the parameters which are used by PicoContainer when it injects the objects to your fields.

Here we’re finally coming to the conclusion of this long introduction. The approach mentioned above does not work with cucumber-java8 because it implies you have the constructor taking no parameters at all. Here in the post we’ll see how to deal with such the case. We’ll see the example that implements 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.

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 taking no parameters. 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.selenium.LazyWebDriver;
import click.webelement.cucumber.pages.HomePage;
import io.cucumber.java8.En;
import org.picocontainer.annotations.Inject;

public class DemonstrateInjectionPrimary implements En {

    @Inject HomePage homePage;
    @Inject LazyWebDriver driver;

    public DemonstrateInjectionPrimary(){
        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);
        });
    }

}

Unlike in pre-java8-style Cucumber where we would inject the shared state through the constructor, here we have to keep the parameters of the constructor empty. Luckily, PicoContainer supports direct field injection. Thus we’re going to use this feature in our step definition. Our steps are to operate with WebDriver and HomePage page object, hence we have to have corresponding fields marked with org.picocontainer.annotations.Inject annotation.

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;
import org.picocontainer.annotations.Inject;


public class DemonstrateInjectionSecondary implements En {

    @Inject
    UselessHomePage uselessHomePage;

    public DemonstrateInjectionSecondary(){
        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

For Dependency Injection support Cucumber provides a facility called ObjectFactory. Having such factory configured, Cucumber calls start() method of the factory once before each scenario and stop() method once after each scenario. Those methods are implemented in the way to trigger your particular DI framework to inject the fields to your objects in scope of the executed scenario and then to dispose the objects where required.

Implementing custom factory

The simplest way to integrate your code with PicoContainer is to make your custom factory delegate the calls to PicoFactory with one small change:

package click.webelement.cucumber.steps.factory;

import click.webelement.cucumber.pages.HomePage;
import click.webelement.cucumber.pages.UselessHomePage;
import click.webelement.cucumber.selenium.LazyWebDriver;
import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.picocontainer.PicoFactory;

public class CustomFactory implements ObjectFactory {

    private PicoFactory delegate = new PicoFactory();

    public CustomFactory(){
        addClass(LazyWebDriver.class);
        addClass(HomePage.class);
        addClass(UselessHomePage.class);
    }

    @Override
    public void start() {
        delegate.start();
    }

    @Override
    public void stop() {
        delegate.stop();
    }

    @Override
    public boolean addClass(Class<?> clazz) {
        return delegate.addClass(clazz);
    }

    @Override
    public <T> T getInstance(Class<T> type) {
        return delegate.getInstance(type);
    }
}

The change I told before is the constructor of your custom factory. This is how you make PicoContainer aware of which classes are to cover with injection mechanism. To make everything go smoothly you should make sure your classes either have a default constructor or constructor taking the arguments of types listed with the help of addClass(..) above.

Register custom factory in Cucumber

Cucumber is designed in the way object factories are to be registered using SPI mechanism. Hence you need to make some configuration steps:

  1. In your resources folder create META-INF/services folder.

  2. In services folder create the file named io.cucumber.core.backend.ObjectFactory

  3. Open that file and add the line click.webelement.cucumber.steps.factory.CustomFactory (amend to what you have in your code)

  4. In resources folder add cucumber.properties file (if you do not have one yet)

  5. Add the following property to the file: cucumber.object-factory=click.webelement.cucumber.steps.factory.CustomFactory (amend to what you have in your code)

So everything is ready now except of one thing. We’re still have no idea what LazyWebDriver is and why we need "useless" scenario in our example.

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 biding 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 our CustomFactory. It delegates the call to PicoFactory 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. 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.