Quoting WikiPedia, here’s the definition of dependency injection:
In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client’s state. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.
This fundamental requirement means that using values (services) produced within the class from new or static methods is prohibited. The class should accept values passed in from outside. This allows the class to make acquiring dependencies someone else’s problem.
The intent behind dependency injection is to decouple objects to the extent that no client code has to be changed simply because an object it depends on needs to be changed to a different one.
Guice is a dependency injection framework provided by Google. If you are looking for a quick start guide, then you can refer to this excellent getting started guide and as always refer here for an exhaustive documentation from Google on Guice.
TestNG supports two forms of dependency injection.
In this post, we are going to look at a specific need viz., being able to wire in a guice parent module into your TestNG execution, without having to do it via your suite xml file.
Imagine the parent guice module to be something like a base class for all other guice modules in your project. The dependency injections that are common across your project would be defined in the parent guice module and the ones that are specific to a particular test method would be provided via the @Guice
annotation.
TestNG lets you wire in a parent guice module via the parent-module
attribute of your suite
tag. The value for this parent-module
attribute would be a fully qualified class name.
The only problem is that TestNG lets you wire in a parent guice module only via your TestNG suite xml file. So you may ask, what’s the issue with that ? Well, what if your project has a wide variety of suite xml files, that are created for various different needs ? You would need to update every single suite xml file to refer to the parent guice module. So its more of a convenience issue than anything else.
No. Not necessarily. There’s an easy way of doing it with the current implementation of TestNG.
Here’s how you do it.
org.testng.IAlterSuiteListener
.XmlSuite
and then set the fully qualified class name of your parent Guice module.org.testng.ITestNGListener
and place it in a folder structure defined as META-INF/services
. If your listeners are production listeners that reside under src/main/java
then this file resides in your src/main/resources
. If your listeners are test listeners that reside under src/test/java
then this file resides in src/test/resources
.For demonstrating this, we are going to be using TestNG 6.13.1
Here’s a sample interface, that represents the ability to query different attributes associated with an execution environment.
public interface EnvInfo {
String getBrowserFlavor();
}
Here’s a concrete implementation of the above mentioned interface.
public class DefaultEnvInfo implements EnvInfo {
@Override
public String getBrowserFlavor() {
//Keeping it simple for the sake of example.
//You can make it fancy by having this read from JVM arguments.
return "firefox";
}
}
Here’s how our default Guice parent module would look like :
import com.google.inject.AbstractModule;
public class DefaultParentModule extends AbstractModule {
@Override
protected void configure() {
bind(EnvInfo.class).to(DefaultEnvInfo.class);
}
}
Here’s how the TestNG listener implementation would look like :
import org.testng.IAlterSuiteListener;
import org.testng.xml.XmlSuite;
import java.util.List;
public class GuiceParentModuleInjector implements IAlterSuiteListener {
@Override
public void alter(List suites) {
XmlSuite suite = suites.get(0);
suite.setParentModule(DefaultParentModule.class.getName());
}
}
Now lets look at how a sample interface which represents the web driver instantiation capabilities looks like:
import org.openqa.selenium.remote.RemoteWebDriver;
public interface WebDriverProducer {
RemoteWebDriver newInstance();
}
Here’s a concrete implementation of the above cited interface:
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import javax.inject.Inject;
public class DefaultWebDriverProducerImpl implements WebDriverProducer {
private String flavor;
@Inject
public DefaultWebDriverProducerImpl(EnvInfo info) {
this.flavor = info.getBrowserFlavor().trim().toLowerCase();
}
@Override
public RemoteWebDriver newInstance() {
switch (flavor) {
case "firefox":
return new FirefoxDriver();
default:
return new ChromeDriver();
}
}
}
Here’s a test specific Guice module.
import com.google.inject.Binder;
import com.google.inject.Module;
public class WebDriverModule implements Module {
@Override
public void configure(Binder binder) {
binder.bind(WebDriverProducer.class).to(DefaultWebDriverProducerImpl.class);
}
}
Here’s a sample test class which will consume the above specified test specific Guice Module and implicitly consume the default Guice Parent module as well.
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
import javax.inject.Inject;
@Guice(modules = WebDriverModule.class)
@Test
public class SampleTestClass {
private RemoteWebDriver driver;
@Inject
public SampleTestClass(WebDriverProducer producer) {
driver = producer.newInstance();
}
public void testMethod() {
driver.get("http://the-internet.herokuapp.com/");
Assert.assertEquals("The Internet", driver.getTitle());
}
@AfterClass
public void cleanup() {
driver.quit();
}
}
A look at how the folder structure looks like for our service loader file
9:12 $ tree src/test/resources/META-INF/services/
src/test/resources/META-INF/services/
└── org.testng.ITestNGListener
0 directories, 1 file
Here’s the contents of the file org.testng.ITestNGListener
~/githome/PlayGround/testbed
09:12 $ cat src/test/resources/META-INF/services/org.testng.ITestNGListener
com.rationaleemotions.github.issue1667.GuiceParentModuleInjector
~/githome/PlayGround/testbed
09:13 $
Here’s a sample execution of all this using TestNG 6.13.1
1516333469849 geckodriver INFO geckodriver 0.19.1
1516333469936 geckodriver INFO Listening on 127.0.0.1:14716
1516333470472 mozrunner::runner INFO Running command: "/Applications/Firefox.app/Contents/MacOS/firefox-bin" "-marionette" "-profile" "/var/folders/mj/81r6v7nn5lqgqgtfl18spfpw0000gn/T/rust_mozprofile.rrBHzcAuHdHV"
1516333471175 Marionette INFO Enabled via --marionette
2018-01-19 09:14:32.082 plugin-container[7336:260934] *** CFMessagePort: bootstrap_register(): failed 1100 (0x44c) 'Permission denied', port = 0xab43, name = 'com.apple.tsm.portname'
See /usr/include/servers/bootstrap_defs.h for the error codes.
1516333472630 Marionette INFO Listening on port 53846
1516333472664 Marionette WARN TLS certificate errors will be ignored for this session
1516333472727 Marionette DEBUG Register listener.js for window 2147483649
Jan 19, 2018 9:14:32 AM org.openqa.selenium.remote.ProtocolHandshake createSession
INFO: Detected dialect: W3C
1516333472869 Marionette DEBUG Received DOM event "beforeunload" for "about:blank"
1516333473477 Marionette DEBUG Received DOM event "pagehide" for "about:blank"
1516333473477 Marionette DEBUG Received DOM event "unload" for "about:blank"
1516333475640 Marionette DEBUG Received DOM event "DOMContentLoaded" for "http://the-internet.herokuapp.com/"
1516333475866 Marionette DEBUG Received DOM event "pageshow" for "http://the-internet.herokuapp.com/"
===============================================
Default Suite
Total tests run: 1, Failures: 0, Skips: 0
===============================================
For any queries, log an issue here.
Tags: TestNG