Testing components
The component model of Quarkus is built on top CDI.
Therefore, Quarkus provides QuarkusComponentTestExtension
- a JUnit extension that makes it easy to test the components/CDI beans and mock their dependencies.
Unlike @QuarkusTest
this extension does not start a full Quarkus application but merely the CDI container and the configuration service.
You can find more details in the Lifecycle section.
This extension is available in the quarkus-junit5-component dependency.
|
1. Basic example
Let’s have a component Foo
- a CDI bean with two injection points.
Foo
componentpackage org.acme;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped (1)
public class Foo {
@Inject
Charlie charlie; (2)
@ConfigProperty(name = "bar")
boolean bar; (3)
public String ping() {
return bar ? charlie.ping() : "nok";
}
}
1 | Foo is an @ApplicationScoped CDI bean. |
2 | Foo depends on Charlie which declares a method ping() . |
3 | Foo depends on the config property bar . @Inject is not needed for this injection point because it also declares a CDI qualifier - this is a Quarkus-specific feature. |
Then a component test could look like:
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.TestConfigProperty;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusComponentTest (1)
@TestConfigProperty(key = "bar", value = "true") (2)
public class FooTest {
@Inject
Foo foo; (3)
@InjectMock
Charlie charlieMock; (4)
@Test
public void testPing() {
Mockito.when(charlieMock.ping()).thenReturn("OK"); (5)
assertEquals("OK", foo.ping());
}
}
1 | The QuarkusComponentTest annotation registers the JUnit extension. |
2 | Sets a configuration property for the test. |
3 | The test injects the component under the test. The types of all fields annotated with @Inject are considered the component types under test. You can also specify additional component classes via @QuarkusComponentTest#value() . Furthermore, the static nested classes declared on the test class are components too. |
4 | The test also injects a mock for Charlie . Charlie is an unsatisfied dependency for which a synthetic @Singleton bean is registered automatically. The injected reference is an "unconfigured" Mockito mock. |
5 | We can leverage the Mockito API in a test method to configure the behavior. |
QuarkusComponentTestExtension
also resolves parameters of test methods and injects matching beans.
So the code snippet above can be rewritten as:
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.TestConfigProperty;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusComponentTest
@TestConfigProperty(key = "bar", value = "true")
public class FooTest {
@Test
public void testPing(Foo foo, @InjectMock Charlie charlieMock) { (1)
Mockito.when(charlieMock.ping()).thenReturn("OK");
assertEquals("OK", foo.ping());
}
}
1 | Parameters annotated with @io.quarkus.test.component.SkipInject are never resolved by this extension. |
Furthermore, if you need the full control over the QuarkusComponentTestExtension
configuration then you can use the @RegisterExtension
annotation and configure the extension programmatically.
The original test could be rewritten like:
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.QuarkusComponentTestExtension;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class FooTest {
@RegisterExtension (1)
static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder().configProperty("bar","true").build();
@Inject
Foo foo;
@InjectMock
Charlie charlieMock;
@Test
public void testPing() {
Mockito.when(charlieMock.ping()).thenReturn("OK");
assertEquals("OK", foo.ping());
}
}
1 | The QuarkusComponentTestExtension is configured in a static field of the test class. |
2. Lifecycle
So what exactly does the QuarkusComponentTest
do?
It starts the CDI container and registers a dedicated configuration object.
If the test instance lifecycle is Lifecycle#PER_METHOD
(default) then the container is started during the before each
test phase and stopped during the after each
test phase.
However, if the test instance lifecycle is Lifecycle#PER_CLASS
then the container is started during the before all
test phase and stopped during the after all
test phase.
The fields annotated with @Inject
and @InjectMock
are injected after a test instance is created.
The parameters of a test method for which a matching bean exists are resolved (unless annotated with @io.quarkus.test.component.SkipInject
or @org.mockito.Mock
) when a test method is executed.
Finally, the CDI request context is activated and terminated per each test method.
3. Injection
Fields of the test class that are annotated with @jakarta.inject.Inject
and @io.quarkus.test.InjectMock
are injected after a test instance is created.
Furthermore, the parameters of a test method for which a matching bean exists are resolved unless annotated with @io.quarkus.test.component.SkipInject
or @org.mockito.Mock
.
There are also some JUnit built-in parameters, such as RepetitionInfo
and TestInfo
, which are skipped automatically.
An @Inject
injection point receives the contextual instance of a CDI bean - the real component under test.
An @InjectMock
injection point receives an "unconfigured" Mockito mock that was created for an unsatisfied dependency automatically.
Dependent beans injected into the fields and test method arguments are correctly destroyed before a test instance is destroyed and after the test method completes, respectively.
Arguments of a @ParameterizedTest method that are provided by an ArgumentsProvider , for example with @org.junit.jupiter.params.provider.ValueArgumentsProvider , must be annotated with @SkipInject .
|
3.1. Tested components
The initial set of tested components is derived from the test class:
-
The types of all fields annotated with
@jakarta.inject.Inject
are considered the component types. -
The types of test methods parameters that are not annotated with
@InjectMock
,@SkipInject
, or@org.mockito.Mock
are also considered the component types. -
If
@QuarkusComponentTest#addNestedClassesAsComponents()
is set totrue
(default) then all static nested classes declared on the test class are components too.
@Inject Instance<T> and @Inject @All List<T> injection points are handled specifically. The actual type argument is registered as a component. However, if the type argument is an interface the implementations are not registered automatically.
|
Additional component classes can be set using @QuarkusComponentTest#value()
or QuarkusComponentTestExtensionBuilder#addComponentClasses()
.
3.2. Auto Mocking Unsatisfied Dependencies
Unlike in regular CDI environments the test does not fail if a component injects an unsatisfied dependency.
Instead, a synthetic bean is registered automatically for each combination of required type and qualifiers of an injection point that resolves to an unsatisfied dependency.
The bean has the @Singleton
scope so it’s shared across all injection points with the same required type and qualifiers.
The injected reference is an unconfigured Mockito mock.
You can inject the mock in your test using the io.quarkus.test.InjectMock
annotation and leverage the Mockito API to configure the behavior.
|
4. Configuration
You can set the configuration properties for a test with the @io.quarkus.test.component.TestConfigProperty
annotation or with the QuarkusComponentTestExtensionBuilder#configProperty(String, String)
method.
If you only need to use the default values for missing config properties, then the @QuarkusComponentTest#useDefaultConfigProperties()
or QuarkusComponentTestExtensionBuilder#useDefaultConfigProperties()
might come in useful.
It is also possible to set configuration properties for a test method with the @io.quarkus.test.component.TestConfigProperty
annotation.
However, if the test instance lifecycle is Lifecycle#_PER_CLASS
this annotation can only be used on the test class and is ignored on test methods.
CDI beans are also automatically registered for all injected Config Mappings. The mappings are populated with the test configuration properties.
5. Mocking CDI Interceptors
If a tested component class declares an interceptor binding then you might need to mock the interception too. There are two ways to accomplish this task. First, you can define an interceptor class as a static nested class of the test class.
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
@QuarkusComponentTest
public class FooTest {
@Inject
Foo foo;
@Test
public void testPing() {
assertEquals("OK", foo.ping());
}
@ApplicationScoped
static class Foo {
@SimpleBinding (1)
String ping() {
return "ok";
}
}
@SimpleBinding
@Interceptor
static class SimpleInterceptor { (2)
@AroundInvoke
Object aroundInvoke(InvocationContext context) throws Exception {
return context.proceed().toString().toUpperCase();
}
}
}
1 | @SimpleBinding is an interceptor binding. |
2 | The interceptor class is automatically considered a tested component. |
Static nested classes declared on a test class that is annotated with @QuarkusComponentTest are excluded from bean discovery when running a @QuarkusTest in order to prevent unintentional CDI conflicts.
|
The second option is to declare an interceptor method directly in the test class; the method is then invoked in the relevant interception phase.
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
@QuarkusComponentTest
public class FooTest {
@Inject
Foo foo;
@Test
public void testPing() {
assertEquals("OK", foo.ping());
}
@SimpleBinding (1)
@AroundInvoke (2)
Object aroundInvoke(InvocationContext context) throws Exception {
return context.proceed().toString().toUpperCase();
}
@ApplicationScoped
static class Foo {
@SimpleBinding (1)
String ping() {
return "ok";
}
}
}
1 | The interceptor bindings of the resulting interceptor are specified by annotating the method with the interceptor binding types. |
2 | Defines the interception type. |