Introduction
@BeforeAll each time you write a test to initialize them.Disclaimer :
This is how our end result will look like :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class TestClass { | |
| @Test | |
| @DisplayName("DemoService should be injected in the method params") | |
| void testMethod(DemoService service) { | |
| // do some work with the service | |
| } | |
| } |
Runner to enable field injection inside your tests .
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package io.github.chermehdi.collections.runners; | |
| import javax.enterprise.inject.se.SeContainer; | |
| import javax.enterprise.inject.se.SeContainerInitializer; | |
| import org.junit.runners.BlockJUnit4ClassRunner; | |
| import org.junit.runners.model.InitializationError; | |
| /** | |
| * @author chermehdi | |
| */ | |
| public class InjectionRunner extends BlockJUnit4ClassRunner { | |
| private SeContainer container; | |
| /** | |
| * Creates a BlockJUnit4ClassRunner to run {@code klass} | |
| * | |
| * @throws InitializationError if the test class is malformed. | |
| */ | |
| public InjectionRunner(Class<?> klass) throws InitializationError { | |
| super(klass); | |
| SeContainerInitializer initializer = SeContainerInitializer.newInstance(); | |
| container = initializer.initialize(); | |
| } | |
| /** | |
| * inject the dependencies of the given object | |
| */ | |
| @Override | |
| protected Object createTest() throws Exception { | |
| Object test = super.createTest(); | |
| return container.select(test.getClass()).get(); | |
| } | |
| } |
The implementation uses CDI to do the injection but that’s just an implementation detail .
and now in your test class you can just do
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package io.github.chermehdi.collections.runners; | |
| import static org.junit.jupiter.api.Assertions.assertEquals; | |
| import javax.inject.Inject; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| /** | |
| * @author chermehdi | |
| */ | |
| @RunWith(InjectionRunner.class) | |
| public class TestUsingRunner { | |
| @Inject | |
| SomeService service; | |
| @Inject | |
| SomeOtherService serviceOther; | |
| @Test | |
| public void helloTest() { | |
| assertEquals("i am a service depend", service.getString()); | |
| } | |
| @Test | |
| public void helloTestAgain() { | |
| assertEquals("i am a service", serviceOther.getString()); | |
| } | |
| } |
This was the old way of doing things, and that was the basics of how spring did its magic using the @RunWith(SpringJUnit4ClassRunner.class) .
Now How can we do the same thing but on the method level, in JUnit4 we couldn’t, but now with the new release it’s quite easy to add support for it .
Extensions and they are classes that extend JUnit5 to do more custom work .the Extention interface is just a marker interface that all extension classes must implement, that been said we can’t do much if we rely on it by itself .Extension interface and that provide more methods that we can hook into and add our custom logic. To register an extension you just annotate your class with @ExtendWith(MyExtension.class), register it programmatically via @RegisterExtension annotated field, or you can do it using the ServiceLoader mechanism.Extension interface called ParameterResolver, this interface defines methods to hook into the test execution and try and resolves test methods parameters at runtime .The example here is going to use Java EE’s Context and Dependency Injection api, and its implementation jboss.weld, but a more abstract way is shown in the repo so make sure to check it out and the demos included for a better understanding of the project .
pom.xml <dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
<version>3.0.1.Final</version>
</dependency>
beans.xml file in our test/resources/META-INF directory (this is specific to CDI)<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" version="1.1"bean-discovery-mode="all"> </beans>
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class DemoService { | |
| public String demo(String value) { | |
| return value + " is a demo"; | |
| } | |
| } |
Now all that’s left is to create the extension and hook in our DI logic :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class InjectionExtension implements ParameterResolver { | |
| private SeContainer container; | |
| /** | |
| * boot the CDI context | |
| */ | |
| public InjectionExtension() { | |
| container = SeContainerInitializer.newInstance().initialize(); | |
| } | |
| /** | |
| * determines weather we can inject all the parameters specified in the test method | |
| */ | |
| @Override | |
| public boolean supportsParameter(ParameterContext parameterContext, | |
| ExtensionContext extensionContext) throws ParameterResolutionException { | |
| Method method = (Method) parameterContext.getDeclaringExecutable(); | |
| Class<?>[] types = method.getParameterTypes(); | |
| return Arrays.stream(types).allMatch(type -> container.select(type).isResolvable()); | |
| } | |
| /** | |
| * resolve the return the object to be used in the test method | |
| */ | |
| @Override | |
| public Object resolveParameter(ParameterContext parameterContext, | |
| ExtensionContext extensionContext) throws ParameterResolutionException { | |
| int paramIndex = parameterContext.getIndex(); | |
| Method method = (Method) parameterContext.getDeclaringExecutable(); | |
| Parameter param = method.getParameters()[paramIndex]; | |
| return container.select(param.getType()).get(); | |
| } | |
| } |
to use the extension all you have to do is
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @author chermehdi | |
| */ | |
| @ExtendWith(InjectionExtension.class) | |
| public class TestExtension { | |
| @Test | |
| void testExtension(DemoService service) { | |
| assertNotNull(service); | |
| } | |
| } |
Explanation
supportsParameter method is called on each parameter found in your test methods, and if it returns true, then resolveParameter is called, if not the test crashes with an error message .SeContainer and we try and resolve the parameter types, by selecting the type and using isResolvable method on the Instance returned .junit-di, the repo includes also demos using CDI and a fixed dependency injection mechanism . Happy Coding …