Using Dagger for Dependency Injection with Cucumber Tests
Why Dependency Injection?
Cucumber is a great framework for building test automation tools with human-readable language and I've used it a fair bit in previous years with Java projects.
One thing you will eventually start to feel is the need for a good dependency injection framework, otherwise your code will be littered with re-creating the same objects to use them, or using things like ThreadLocal
or static
s to share state, which isn't ideal.
At the time of writing, Cucumber (7.1.0) still recommends using PicoContainer as the default dependency injection framework, and it's been blogged about a fair bit before, such as the wonderful Angie Jones' blog post Sharing State Between Cucumber Steps with Dependency Injection.
However as I've blogged about, PicoContainer isn't ideal because it requires zero-args constructors, which isn't always possible.
Why Dagger?
although there are other dependency injection frameworks called out in the Cucumber docs for Sharing state between steps, I instead recommend Dagger. As noted in Lightweight and Powerful Dependency Injection for JVM-based Applications with Dagger, Dagger is a great, super lightweight framework that works really nicely, and is fairly easy to get set up in a project.
I'd very much recommend a read of that post for a bit more about why Dagger is great, and we'll discuss at the end of the post why you may not see it very often.
If you're comfortable with using Spring as your dependency injection framework, especially if using Spring Boot, I'd recommend continuing with it so we don't have to maintain two styles of dependency injection setup.
Base setup
Example code can be found on a branch on GitLab.
Let us say that we're following a common practice of using constructor injection for objects, such as the below code snippet:
package io.cucumber.skeleton;
import io.cucumber.java.en.Given;
public class StepDefinitions {
private final Belly belly;
public StepDefinitions(Belly belly) {
this.belly = belly;
}
@Given("I have {int} cukes in my belly")
public void I_have_cukes_in_my_belly(int cukes) {
belly.eat(cukes);
}
}
And the following Belly
class definition:
package io.cucumber.skeleton;
public class Belly {
public void eat(int cukes) {
}
}
Migrating to Dagger
To be able to migrate to Dagger, we'd follow a similar setup as mentioned in my previous Dagger article, which is to set up the dependencies (in this example for Gradle, when running Cucumber tests from src/test/resources
):
dependencies {
+ testAnnotationProcessor("com.google.dagger:dagger-compiler:2.40.5")
+ testImplementation("com.google.dagger:dagger:2.40.5")
testImplementation(platform("org.junit:junit-bom:5.8.2"))
testImplementation(platform("io.cucumber:cucumber-bom:7.1.0"))
testImplementation("io.cucumber:cucumber-java")
- testImplementation("io.cucumber:cucumber-picocontainer")
testImplementation("io.cucumber:cucumber-junit-platform-engine")
testImplementation("org.junit.platform:junit-platform-suite")
testImplementation("org.junit.jupiter:junit-jupiter")
Then we'd set up a Config
class that would define which of the objects to be injected:
package io.cucumber.skeleton.config;
import dagger.Component;
import io.cucumber.skeleton.Belly;
import javax.inject.Singleton;
@Singleton
@Component(modules = {ConfigModule.class})
public interface Config {
Belly belly();
}
Which are produced by the configuration module:
import dagger.Module;
import dagger.Provides;
import io.cucumber.skeleton.Belly;
import javax.inject.Singleton;
@Module
public class ConfigModule {
private ConfigModule() {
throw new UnsupportedOperationException("Utility class");
}
@Provides
@Singleton
public static Belly belly() {
return new Belly();
}
}
Which we need to modify the StepDefinitions
, to utilise the new Dagger-built configuration:
package io.cucumber.skeleton;
import io.cucumber.java.en.Given;
+import io.cucumber.skeleton.config.DaggerConfig;
public class StepDefinitions {
private final Belly belly;
- public StepDefinitions(Belly belly) {
- this.belly = belly;
+ public StepDefinitions() {
+ this.belly = DaggerConfig.create().belly();
}
@Given("I have {int} cukes in my belly")
Allowing step definition classes to be unit testable
Notice that this has resulted in us using zero-args constructor in the step definitions - unless we're unit testing our steps this will be fine, and if we are, we can provide a test-only constructor to inject in mock configuration:
package io.cucumber.skeleton;
import io.cucumber.java.en.Given;
import io.cucumber.skeleton.config.Config;
import io.cucumber.skeleton.config.DaggerConfig;
public class StepDefinitions {
private final Belly belly;
public StepDefinitions() {
this(DaggerConfig.create());
}
StepDefinitions(Config config) {
this.belly = config.belly();
}
@Given("I have {int} cukes in my belly")
public void I_have_cukes_in_my_belly(int cukes) {
belly.eat(cukes);
}
}
Configurability
As mentioned in Lightweight and Powerful Dependency Injection for JVM-based Applications with Dagger, this can be improved by adding in a Builder
that can then take environment specific configuration, allowing you to i.e. set up your tests to run differently against different environments.
Caveats
Notice that there's no first-class support in Cucumber's dependency tree - this is because Dagger configuration is very personal to the project, so there's no out-of-the-box way to do it.
I'm going to look at working with others on the Cucumber team to see if there's a way we can do this, and even if it's not first-class support, there may be a way we can produce a dependency that makes it easier to utilise.