Site icon Vinsguru

Selenium WebDriver – How To Inject Page Object Dependencies Using Guice

Overview:

In this article, we will see the use of Dependency Injection using Guice lib and how it helps us to come up with more readable, reusable page objects and tests.

Dependency Injection:

Lets this consider this simple Page Object for Google page.

public class Google {

    private final WebDriver driver = new ChromeDriver();

    public void goTo() {
        this.driver.get("https://wwww.google.com");
    }

}

Assuming chrome driver server system properties have been set already, there is nothing wrong here. The code will just work fine.

Google google = new Google();
google.goTo();

But what will happen if we need to make the test work for a different browser? We have to modify the class, right? Because it is tightly coupled. How many classes will we modify like this?

Lets see if we could modify slightly as shown below.

public class Google {

    private final WebDriver driver;

    public Google(WebDriver driver) {
        this.driver = driver;
    }

    public void goTo() {
        this.driver.get("https://wwww.google.com");
    }

}

In this above class, we do not want to create the new instance of the WebDriver. But it is a dependency required for this class to function as expected. So, We have designed the class in such a way that this dependency will be injected by the test / whoever creates the instance of this class. This approach makes the classes loosely coupled, but the dependencies have to be injected by the invoking classes. If it is a page object, the testng/junit tests should take care of creating the WebDriver instance and pass it to the constructor of this class.

Google google = new Google(driver); //constructor injection
google.goTo();

We could also inject the dependency by using setters method.

Google google = new Google();
google.setDriver(driver); //setter injection
google.goTo();

The Google page object has the hard coded URL. But in reality, we need to run our automated tests in Dev / QA / Staging / PROD environments. So we can not hard code the URL in our page objects. So even the URL should be injected.

public class Google {

    private final WebDriver driver;
    private final String URL;

    public Google(WebDriver driver, String URL) {
        this.driver = driver;
        this.URL = URL;
    }

    public void goTo() {
        this.driver.get(URL);
    }
}

Hm… It looks better now, rt?  Yeah, the page object might look better. What about the junit/testng test class which is going to use it? The test class has to take care of all the dependencies required for this page object. With all the driver instance creation logic, fetching the url from some property files etc, the test class will look ugly for sure!!!

As part of our functional test automation, we might have created tons of page objects with a lot of other dependencies.  Creating & maintaining the life cycle of the driver, page objects, utility classes like property file readers, reporting utility, logger etc make the framework very difficult to maintain.

Creating instances are painful sometimes. For ex: WebDriver.  If you are not using a Factory Pattern as mentioned in this article and you are directly creating a WebDriver instance in the Test class/Page object itself, It will make your class bloated, less readable and difficult to maintain.

How can we have an automated dependency injection in the framework? How can we write less number of code & achieve whatever we wanted to have?

There is a library from Google which could help us here.

Guice (pronounced as ‘juice’)

Guice is a Dependency Injection framework and most of the Java developers use it in their applications. Lets see how we can take advantage of Guice in Functional Test automation frameworks.

This article might not be able to cover all the features of Guice. I would suggest you to spend some time here in reading more about Guice.

Guice – Binding:

Guice can directly inject the instance of a class with default constructor when you use @Inject annotation.

For ex:

Google google = new Google(driver, url);

can be replaced by simply using

@Inject
Google google;

I use a Factory Pattern for a WebDriver instance creation. I need to map the ChromeDriverManager.class to the DriverManager.class. When you have constructor with arguments or try to map a concrete class to an interface/abstract class, then we need to do few configurations as shown here.

public class DriverModule extends AbstractModule {

    @Override
    protected void configure() {

        bind(DriverManager.class)
            .to(ChromeDriverManager.class)
            .in(Scopes.SINGLETON);

    }

}

Now Guice can inject an instance of ChromeDriverManager.class into the driverManager variable.

@Inject
DriverManager driverManager;

If I had to code to get a required instance, I could use @Provides method. For ex, I might not be really interested in DriverManager.class. But I need that to get the WebDriver instance.

public class DriverModule extends AbstractModule {

    @Override
    protected void configure() {

        bind(DriverManager.class)
            .to(ChromeDriverManager.class)
            .in(Scopes.SINGLETON);

    }
    @Provides
    public WebDriver getDriver(DriverManager driverManager) {
        return driverManager.getDriver();
    }

}

Now Guice can inject an instance of ChromeDriver directly into the driver variable by hiding a lot of code for the creation logic.

@Inject
WebDriver driver;

If you need different browser instances in the same test for some reason, Guice can inject that too!!

@Inject @Chrome
WebDriver chrome;

@Inject @Firefox
WebDriver firefox;

Here @Chrome and @Firefox are custom annotations for Guice to understand what to inject. More information is here.

Not only the WebDriver instance. We might be interested in Actions, JavascriptExecutor,Logger etc. So Guice can inject these instances too from the WebDriver instance we create. It can even the inject the value of a property from your property files.

Guice – Usage In Test Framework:

Lets assume that We keep all our application url, user credentials, any other input parameters in a property file as shown here.

We could also maintain 1 property file for each environment where we run our automated tests.

As I use Google search functionality for example, I design page objects as shown here by using Single Responsibility principle.

GoogleSearchWidget class will look like this. WebDriver instance will be injected by Guice. So, I add an @Inject in the constructor.

public class GoogleSearchWidget {

    @FindBy(name = "q")
    WebElement searchBox;

    @FindBy(name = "btnG")
    WebElement searchButton;

    @Inject
    public GoogleSearchWidget(WebDriver driver) {
        PageFactory.initElements(driver, this);
    }

    public void searchFor(String txt) {
        searchBox.sendKeys(txt);
        searchButton.click();
    }

}

GoogleSearchResult class will look like this. I again add an @Inject in the constructor.

public class GoogleSearchResult {

    @FindBy(className = "rc")
    private List<WebElement> results;

    @Inject
    public GoogleSearchResult(WebDriver driver) {
        PageFactory.initElements(driver, this);
    }

    public int getCount() {
        return results.size();
    }

    public void displayResult() {
        results.stream()
            .map(WebElement::getText)
            .forEach(System.out::println);
    }
}

Google class should have the instance of these above 2 classes and few other instances like JavascriptExecutor, Actions etc. To get the property value, We need to use @Named(property-key). So the private String URL in the below class will have the value of the ‘application.url’ which is ‘https://google.com’.

public class Google {

    private final WebDriver driver;

    @Inject
    @Named("application.url")
    private String URL;

    @Inject
    private GoogleSearchWidget searchBox;

    @Inject
    private GoogleSearchResult searchResult;

    @Inject
    private Actions actions;

    @Inject
    private JavascriptExecutor jsExecutor;

    @Inject
    public Google(WebDriver driver) {
        this.driver = driver;
    }

    public void goTo() {
        this.driver.get(this.URL);
    }

    public GoogleSearchWidget getSearchWidget() {
        return searchBox;
    }

    public GoogleSearchResult getResults() {
        return searchResult;
    }

    public Object execute(String script) {
        return jsExecutor.executeScript(script);
    }

}

Now It is time for us to modify the config module class as shown below. GoogleSearchResults / GoogleSearchWidget does not need any config. But the JavascriptExecutor/Actions are created from WebDriver instance. So, we need to help guice how it has to create the instance.

public class DriverModule extends AbstractModule {

    @Override
    protected void configure() {

        //DriverManager config
        bind(DriverManager.class)
            .to(ChromeDriverManager.class)
            .in(Scopes.SINGLETON);

        //My test input properties
        try {
            Properties props = new Properties();
            props.load(new FileInputStream("uat.properties"));
            Names.bindProperties(binder(), props);
        } catch (IOException e) {
            //skip
        }

    }

    @Provides
    public WebDriver getDriver(DriverManager driverManager) {
        return driverManager.getDriver();
    }

    @Provides
    public Actions getActions(WebDriver driver) {
        return new Actions(driver);
    }

    @Provides
    public JavascriptExecutor getExecutor(WebDriver driver) {
        return (JavascriptExecutor)(driver);
    }
}

That is it. We are good to use these page objects and properties in our tests. I use Testng framework which has support for including Guice module.

@Guice(modules = {
    DriverModule.class
})
public class GuiceTest {

    @Inject
    Google google;


    @Test(dataProvider = "google-test")
    public void f(String txt, String color) throws InterruptedException {

        google.goTo();
        
        //change the color of the google page
        google.execute("document.body.style.backgroundColor='" + color + "';");

       //do search and show results
        google.getSearchWidget().searchFor(txt);
        google.getResults().displayResult();

    }

    @DataProvider(name = "google-test")
    public static Object[][] getData() {
        return new Object[][] {
            {
                "guru",
                "blue"
            }, {
                "guice",
                "green"
            }
        };
    }

}

Now my test class looks neat and clean. If I need any object in my test/page objects, I could just use @Inject.

Summary:

By injecting all the dependencies automatically, Guice can increase our productivity. It makes our code readable, reusable. Due to these benefits, The code becomes easy to maintain as well.

For advanced usage of Guice with WebDriver, check this article.

Happy Testing and Subscribe 🙂

 

Share This:

Exit mobile version