Implementing Page Object pattern with Selenium, Cucumber and Guice

Sharing a state in a proper way in Cucumber scenarios is somewhat a lot of people struggle of. All because Cucumber (when runs a scenario) might take step definitions from different classes and there is no trivial way to make them use shared instances of objects.

Fortunately Cucumber offers a solution for that. It supports a number of Dependency Injection frameworks which can make object sharing easy and effective.

Today we’re going to talk about Google Guice DI framework that is one of DI frameworks supported by Cucumber. In the article we’ll look at a sample scenario that would implement Page Object pattern with the help of Selenium and its PageFactory harness.

Example description

In this example we’ll prepare a simple scenario that would be opening a page and wait for given period of time. Gherkin code for that scenario will be as follows:

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
    And Navigate back

    Examples:
      | time           |
      | 5              |
      | 10             |

There will be WebDriver object that will be shared between steps in step definitions class and a hook method in dedicated Hook class. In overall the file structure will be looking like this:

src
└── test
    ├── java
    │   └── click
    │       └── webelement
    │           └── cucumber
    │               ├── pages
    │               │   └── HomePage.java _____________________ (1)
    │               └── steps
    │                   └── definitions
    │                       ├── guice
    │                       │   ├── LazyDriver.java ___________ (2)
    │                       │   ├── PomInjectorSource.java ____ (3)
    │                       │   └── PomModule.java ____________ (4)
    │                       ├── Hooks.java ____________________ (5)
    │                       └── StepsWithInjection.java _______ (6)
    └── resources
        ├── click
        │   └── webelement
        │       └── cucumber
        │           └── guice_demo.feature ____________________ (7)
        └── cucumber.properties _______________________________ (8)

Let’s briefly stop by each of the items:

  1. Page Object that is basically implemented in regular way with only a bit of extra annotations required by Guice

  2. A wrapper for WebDriver that supports lazy initialization. Actually our solution would work without that element, however if you have scenarios where a driver is not used at all such the implementation will prevent instantiating driver object where it is not actually used.

  3. This will help to integrate our custom classes we want to support the injection for with what Cucumber already knows how to inject.

  4. Special mapping telling Guice what to inject to which fields/parameters

  5. Hook method that is executed at the end of scenario in order to dispose WebDriver object.

  6. Step definition class

  7. Feature file holding the description of our sample scenario

  8. Configure a property to tell Cucumber we’re using the mechanism implemented at (3)

Alright. Now let’s move on to a bit of theory.

A bit of theory

Dependency Injection is a paradigm that implies decoupling of logic of how you use the fields and how you assign the values to fields (which are effectively are the dependencies of your object). Following such paradigm you design an object assuming the dependencies are injected in a proper way and then design what that "proper way" is.

Guice

Google Guice is on of the DI frameworks which Cucumber supports. What does the support mean? Cucumber has its own mechanism to instantiate objects called ObjectFactory. So, Cucumber integrates DI framework into that own architecture.

There are three core concepts in Guice which are: Scopes, Modules and Injectors.

Guice Scopes

The scope in DI is a set of conditions under which a framework injects the same instance (it has already injected before) to a field of other object or parameter of its constructor on construction phase. As an example there could be Guice predefind scope called Singleton. The latter implies that Guice will be injecting the same object instance to all the required fields on the entire lifetime of the application.

Cucumber supplies its custom scope ScenarioScope that ensures that the fields of types applied to such scope will be assigned with the same object instance on the lifetime of the scenario.

Guice Modules

A Module is Guice concept that is responsible for building injection rules. Using a module you can tell Guice that "All fields or parameters of type WebDriver will be assigned with certain object of type FirefoxDriver" or "Any String field annotated with @MyProp will be assigned with value `My value`" and many more options.

Cucumber supplies io.cucumber.guice.ScenarioModule to support proper integration of its custom scope into Guice core. However there could be much more modules that implement your own rules. Those new modules have to be somehow integrated as well. We’ll see at that later.

Guice Injectors

Injector is the central concept of everything in Guice. It combines everything at one point and can create objects considering the rules you have set up in modules (when you create an injector you list the modules it will be using to read injection rules from) and taking into account the scopes.

Cucumber-Guice integration high-level overview

The library cucumber-guice has the following core components that facilitate the integration:

  • io.cucumber.guice.GuiceFactory

  • META-INF/services/io.cucumber.core.backend.ObjectFactory

  • io.cucumber.guice.InjectorSource

  • io.cucumber.guice.ScenarioModule

  • io.cucumber.guice.SequentialScenarioScope

The core of integration is GuiceFactory that implements ObjectFactory interface. It integrates Guice injector into the mechanism of object instantiating that is used by Cucumber. That factory is registered in Cucumber using SPI (through META-INF/services/ folder). The factory also uses InjectorSource in order to supply custom modules to the injector (we’ll need to implement our own source for the test and use special option to enable it).

Implement the test

We have gherkin script already so that we won’t show it again here. Better we start from a page object that has minimal difference from its classic implementation.

HomePage.java

package click.webelement.cucumber.pages;

import com.google.inject.Inject;
import io.cucumber.guice.ScenarioScoped;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

@ScenarioScoped
public class HomePage {

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

    @Inject
    public HomePage(WebDriver 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.");
    }

}

Let’s look at the code. Everything looks familiar (for those who has the experience of implementing page objects in Selenium). Except couple of annotations. @ScenarioScoped tells Guice that being instantiated, the instance of this class will have to exist until the end of current scenario. So wherever we inject the field of HomePage type it will be referring to the same instance on the scenario lifetime extent.

@Inject annotation tells Guice that the object has to be constructed using annotated constructor. Guice then will use the same approach to create an instance of WebDriver class to use as the constructor parameter.

StepsWithInjection.java

Let’s now look at the step definition code

package click.webelement.cucumber.steps.definitions;

import click.webelement.cucumber.pages.HomePage;
import com.google.inject.Inject;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import org.openqa.selenium.WebDriver;

public class StepsWithInjection {

    @Inject
    HomePage homePage;
    @Inject
    WebDriver driver;

    @Given("Open page {}")
    public void openPage(String url) {
        System.out.println("Opening url: " + url);
        driver.get(url);
    }

    @Then("Go to All Posts")
    public void goToAllPosts() {
        homePage.goToAllPosts();
    }

    @And("Stare for {int} seconds")
    public void stare(Integer seconds) throws InterruptedException {
        System.out.println("Staring at the page for "
                + seconds.toString()
                + " seconds..");
        Thread.sleep(seconds * 1000);
    }

    @And("Navigate back")
    public void navigateBack() {
        driver.navigate().back();

    }

}

It also looks pretty straightforward. Here we also can see @Inject annotation. This is a bit different approach to injection supported by Guice. It is less recommended but I found it more compact and prefer over the constructor injection if there is no other logic on construction phase than just assigning values to the fields.

Hooks.java

Unfortunately Guice do not have a mechanism to dispose objects that holds the resources (hence the official Guice position is to limit the injection of such types and inject them with caution) when the logic is exiting the scope. WebDriver is just such a case - we need to free up a session when end our tests with driver.quit(). Cucumber has "hooks" which allow to bind certain actions before and after the scenarios so we’re going to use a hook to dispose current driver instance.

package click.webelement.cucumber.steps.definitions;

import com.google.inject.Inject;
import io.cucumber.java.After;
import org.openqa.selenium.WebDriver;

public class Hooks {

    @Inject
    WebDriver driver;

    @After
    public void tearDown(){
        if(driver != null){
            driver.quit();
        }
    }

}

Injection here works in the exactly same way it works with any other objects under Cucumber control.

LazyDriver.java

Sometimes you run the scenarios which do not use WebDriver_s. However they might use steps defined within classes that have _WebDriver injected. In such cases when Cucumber instantiates step definition class it injects WebDriver field as well. It means that driver object is being created at that point with all the consequences like opening browser window, executing driver process and so on.

There is a workaround for such issue. We create a wrapper that initializes a driver only when there is a first call to any driver method happens. Hence for the scenarios where the driver is not actually used, it will take only a bit of memory to hold object structure and won’t be running any external processes or execute any other logic.

package click.webelement.cucumber.steps.definitions.guice;

import com.google.inject.Inject;
import io.cucumber.guice.ScenarioScoped;
import org.openqa.selenium.*;
import org.openqa.selenium.remote.RemoteWebDriver;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Set;

@ScenarioScoped
public class LazyDriver implements WebDriver {

    private WebDriver delegate = null;

    private Capabilities capabilities;

    private String driverUrl;

    @Inject
    public LazyDriver(Capabilities capabilities, @DriverURL String driverUrl){
        this.capabilities = capabilities;
        this.driverUrl = driverUrl;
    }

    private WebDriver getDelegate() {
        if (delegate == null) {
            try {
                System.out.println("Creating lazy initialization...");
                delegate = new RemoteWebDriver(new URL(driverUrl), capabilities);
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
        }
        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();
    }

}

As you can notice here we also use @ScenarioScoped annotation that tells Guice that we need to preserve this instance for all the places requiring WebDriver on the extent of Scenario execution.

There is also an annotation that we are not yet familiar with. It is @DriverURL. This annotation is our custom one. It is the qualifier which is the recommended way to inject properties (like driver URL). Having no such technique we would not be able to inject different values to different fields of the same type (like String).

Here I’m using constructor injection because direct injection seems not be working with qualifiers. And yes.. Where is that "qualifier" defined? It is defined in the module that we’re now proceeding to.

PomModule.java

Now we proceed to very important step. Here in the module we’re going to describe the rules Guice will be following when inject objects to the fields.

package click.webelement.cucumber.steps.definitions.guice;

import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import javax.inject.Qualifier;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

public class PomModule extends AbstractModule {

    @Override
    protected void configure() {
        bind(WebDriver.class).to(LazyDriver.class);     // (1)
    }

    @Provides
    public Capabilities getCapabilities(){
        return new ChromeOptions();                     // (2)
    }

    @Provides @DriverURL
    public String getDriverUrl(){
        return "http://selenium-hub:4444";              // (3)
    }

}

@Qualifier
@Retention(RUNTIME)
@interface DriverURL {}

A module is a unit of configuration telling Guice how it has to behave when encountering a field or a parameter of a certain type. Any module has to extend com.google.inject.AbstractModule and can use one of two ways to configure. Either a binding DSL in configure() method or the methods annotated with @Provides annotation. The latter one is the preferable way according to Guice recommendations however it is not as flexible as DSL.

Let’s get through commented parts of the code to have some explanation of what’s going on there:

  1. Here we’re telling Guice that wherever it encounters WebDriver field or parameter it has to instantiate (if it hasn’t been yet according to applied scope) LazyDriver class and pass to that field/parameter.

  2. Wherever we meet field/parameter of Capabilities we must supply object of ChromeOptions type

  3. Wherever we meet parameter of String type and annotated with @DriverURL annotation we must supply http://selenium-hub:4444 value to that parameter

In the same file we define DriverURL qualifier that was used here and at previous step in LazyDriver class.

PomInjectorSource.java

We can have as many modules as we need but by default Cucumber has the only one module that defines its internal injections. Fortunately it supports a mechanism to add more modules to the run. Below is the implementation:

package click.webelement.cucumber.steps.definitions.guice;

import com.google.inject.Guice;
import com.google.inject.Injector;
import io.cucumber.guice.CucumberModules;
import io.cucumber.guice.InjectorSource;

public class PomInjectorSource implements InjectorSource {

    @Override
    public Injector getInjector() {
        return Guice.createInjector(
                CucumberModules.createScenarioModule(),
                new PomModule());
    }
}

Here we list modules that we’re going to use for our scenario. Do not forget to list Cucumber module as well.

In order to register this InjectorSource in Cucumber we need to supply a special option. For example we can do that using cucumber.properties file. Add the following property to the file:

guice.injector-source=click.webelement.cucumber.steps.definitions.guice.PomInjectorSource

Conclusion

Alright. Now we have wired everything together and have a complete picture of a typical implementation of a Page Object Model in Selenium where you use Guice as Dependency Injection framework. If you still have the questions please send them to me using this form. I will amend the article according to your feedback.