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:
-
Page Object that is basically implemented in regular way with only a bit of extra annotations required by Guice
-
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.
-
This will help to integrate our custom classes we want to support the injection for with what Cucumber already knows how to inject.
-
Special mapping telling Guice what to inject to which fields/parameters
-
Hook method that is executed at the end of scenario in order to dispose WebDriver object.
-
Step definition class
-
Feature file holding the description of our sample scenario
-
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:
-
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. -
Wherever we meet field/parameter of
Capabilities
we must supply object ofChromeOptions
type -
Wherever we meet parameter of
String
type and annotated with@DriverURL
annotation we must supplyhttp://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.