Skip to content

Commit 141bffd

Browse files
authored
Merge pull request #1738 from mkouba/quarkus-component-test
QuarkusComponentTest blogpost
2 parents 20c097f + e1d82ea commit 141bffd

File tree

1 file changed

+277
-0
lines changed

1 file changed

+277
-0
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
---
2+
layout: post
3+
title: 'Explore a new way of testing CDI components in Quarkus'
4+
date: 2023-07-10
5+
tags: testing
6+
synopsis: 'Quarkus 3.2 introduced an experimental feature to ease the testing of CDI components and mocking of their dependencies.'
7+
author: mkouba
8+
---
9+
10+
The Quarkus component model is built on top of CDI.
11+
However, writing unit tests for beans without a running CDI container is often a tedious work.
12+
Without the container services up and running, all the work has to be done manually.
13+
First of all, no dependency injection is performed.
14+
Furthermore, no events are fired and no observers are notified.
15+
Also, interceptors are not applied.
16+
In short, everything needs to be wired together by hand.
17+
But Quarkus can do better, right?
18+
Of course, it can!
19+
Quarkus 3.2 introduced an experimental feature to ease the testing of CDI components and mocking of their dependencies.
20+
It's a lightweight JUnit 5 extension that does not start a full Quarkus application but merely runs the services needed to make the testing a joyful experience.
21+
22+
== A simple example
23+
24+
First of all, add the `quarkus-junit5-component` module dependency to your project.
25+
26+
[role="primary asciidoc-tabs-sync-maven"]
27+
.Maven
28+
****
29+
[source,xml,subs=attributes+]
30+
----
31+
<dependency>
32+
<groupId>io.quarkus</groupId>
33+
<artifactId>quarkus-junit5-component</artifactId>
34+
<scope>test</scope>
35+
</dependency>
36+
----
37+
****
38+
[role="secondary asciidoc-tabs-sync-gradle"]
39+
.Gradle
40+
****
41+
[source,groovy,subs=attributes+]
42+
----
43+
dependencies {
44+
testImplementation("io.quarkus:quarkus-junit5-component")
45+
}
46+
----
47+
****
48+
49+
Now, imagine that we have a component `Foo` which is an `@ApplicationScoped` CDI bean.
50+
51+
[source,java]
52+
----
53+
package org.acme;
54+
55+
import jakarta.enterprise.context.ApplicationScoped;
56+
import jakarta.inject.Inject;
57+
58+
@ApplicationScoped
59+
public class Foo {
60+
61+
@Inject
62+
Charlie charlie; <1>
63+
64+
@ConfigProperty(name = "bar") <2>
65+
boolean bar;
66+
67+
public String ping() { <3>
68+
return bar ? charlie.ping() : "nok";
69+
}
70+
}
71+
----
72+
<1> `Foo` depends on `Charlie` which declares a method `ping()`.
73+
<2> `Foo` depends on the config property `bar`.
74+
<3> The goal is to test this method which makes use of the `Charlie` dependency and the `bar` config property.
75+
76+
Then, a simple component test looks like this:
77+
78+
[source,java]
79+
----
80+
import static org.junit.jupiter.api.Assertions.assertEquals;
81+
82+
import jakarta.inject.Inject;
83+
import io.quarkus.test.InjectMock;
84+
import io.quarkus.test.component.TestConfigProperty;
85+
import io.quarkus.test.component.QuarkusComponentTest;
86+
import org.junit.jupiter.api.Test;
87+
import org.mockito.Mockito;
88+
89+
@QuarkusComponentTest <1>
90+
@TestConfigProperty(key = "bar", value = "true") <2>
91+
public class FooTest {
92+
93+
@Inject
94+
Foo foo; <3>
95+
96+
@InjectMock
97+
Charlie charlieMock; <4>
98+
99+
@Test
100+
public void testPing() {
101+
Mockito.when(charlieMock.ping()).thenReturn("OK"); <5>
102+
assertEquals("OK", foo.ping());
103+
}
104+
}
105+
----
106+
<1> `@QuarkusComponentTest` registers the `QuarkusComponentTestExtension` that does all the heavy lifting under the hood.
107+
<2> `@TestConfigProperty` is used to set the value of a configuration property for the test.
108+
<3> The test injects the tested component. The types of all fields annotated with `@Inject` are considered the component types under test.
109+
<4> The test also injects a mock of `Charlie`, a dependency for which a `@Singleton` bean is registered automatically. The injected reference is an "unconfigured" Mockito mock.
110+
<5> The Mockito API is used to configure the behavior of the injected mock.
111+
112+
In this particular test, the only "real" component under the test is `org.acme.Foo`.
113+
The `Charlie` dependency is a mock that is created automatically.
114+
And the value of the `bar` configuration property is set with the `@TestConfigProperty` annotation.
115+
116+
== How does it work?
117+
118+
The `QuarkusComponentTestExtension` does several things during the `before all` test phase.
119+
It starts ArC - the CDI container in Quarkus.
120+
It also registers a dedicated configuration object.
121+
The container is then stopped and the config is released during the `after all` test phase.
122+
The fields annotated with `@Inject` and `@InjectMock` are injected after a test instance is created.
123+
Finally, the CDI request context is activated and terminated per each test method.
124+
125+
NOTE: By default, a new test instance is created for each test method. Therefore, a new CDI container is started for each test method. However, if the test class is annotated with `@org.junit.jupiter.api.TestInstance` and the test instance lifecycle is set to `org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS` then the CDI container will be shared across all test method executions of a given test class.
126+
127+
=== Tested components
128+
129+
By default, the types of all fields annotated with `@Inject` are considered component types.
130+
However, you can also specify additional test components: either with the `@QuarkusComponentTest#value()` or programmatically as the arguments of the <<advanced_features,`QuarkusComponentTestExtension(Class<?>...)`>> constructor.
131+
Finally, the static nested classes declared on the test class are components too.
132+
133+
TIP: Static nested classed declared on a test class that is annotated with `@QuarkusComponentTest` are excluded from bean discovery when running a regular `@QuarkusTest` in order to prevent unintentional CDI conflicts.
134+
135+
=== Automatic mocking of unsatisfied dependencies
136+
137+
Unlike in regular CDI environments, the test does not fail if a component injects an unsatisfied dependency.
138+
Instead, a mock bean is registered automatically for each combination of required type and qualifiers of an injection point that resolves to an unsatisfied dependency.
139+
The mock bean has the `@Singleton` scope so it’s shared across all injection points with the same required type and qualifiers.
140+
And the injected reference is an unconfigured Mockito mock.
141+
This mock can be injected in the test with `@io.quarkus.test.InjectMock` and configured with the Mockito API.
142+
143+
=== Configuration
144+
145+
A dedicated `SmallRyeConfig` is registered during the `before all` test phase.
146+
It’s possible to set the configuration properties with the `@TestConfigProperty` annotation or programmatically with the `QuarkusComponentTestExtension#configProperty(String, String)` method.
147+
If you need to use the default values for missing config properties, then `@QuarkusComponentTest#useDefaultConfigProperties()` and `QuarkusComponentTestExtension#useDefaultConfigProperties()` might come in useful.
148+
149+
[[advanced_features]]
150+
== Advanced features
151+
152+
It is possible to configure the `QuarkusComponentTestExtension` programatically.
153+
The simple example above could be rewritten like:
154+
155+
[source,java]
156+
----
157+
import static org.junit.jupiter.api.Assertions.assertEquals;
158+
159+
import jakarta.inject.Inject;
160+
import io.quarkus.test.InjectMock;
161+
import io.quarkus.test.component.QuarkusComponentTestExtension;
162+
import org.junit.jupiter.api.Test;
163+
import org.mockito.Mockito;
164+
165+
public class FooTest {
166+
167+
@RegisterExtension <1>
168+
static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension()
169+
.configProperty("bar","true");
170+
171+
@Inject
172+
Foo foo;
173+
174+
@InjectMock
175+
Charlie charlieMock;
176+
177+
@Test
178+
public void testPing() {
179+
Mockito.when(charlieMock.ping()).thenReturn("OK");
180+
assertEquals("OK", foo.ping());
181+
}
182+
}
183+
----
184+
<1> Annotate a `static` field of type `QuarkusComponentTestExtension` with the `@RegisterExtension` annotation and configure the extension programmatically.
185+
186+
Sometimes you need full control over the bean attributes and maybe even configure the default behavior of a mocked dependency.
187+
In this case, the mock configurator API and the `QuarkusComponentTestExtension#mock()` method is the right choice.
188+
189+
[source,java]
190+
----
191+
import static org.junit.jupiter.api.Assertions.assertEquals;
192+
193+
import jakarta.enterprise.context.Dependent;
194+
import jakarta.inject.Inject;
195+
import io.quarkus.test.InjectMock;
196+
import io.quarkus.test.component.QuarkusComponentTestExtension;
197+
import org.junit.jupiter.api.Test;
198+
import org.mockito.Mockito;
199+
200+
public class FooTest {
201+
202+
@RegisterExtension
203+
static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension()
204+
.configProperty("bar","true")
205+
.mock(Charlie.class)
206+
.scope(Dependent.class) <1>
207+
.createMockitoMock(mock -> {
208+
Mockito.when(mock.pong()).thenReturn("BAR"); <2>
209+
});
210+
211+
@Inject
212+
Foo foo;
213+
214+
@Test
215+
public void testPing() {
216+
assertEquals("BAR", foo.ping());
217+
}
218+
}
219+
----
220+
<1> The scope of the mocked bean is `@Dependent`.
221+
<2> Configure the default behavior of the mock.
222+
223+
=== Mocking CDI interceptors
224+
225+
NOTE: This feature is only available in Quarkus 3.3+.
226+
227+
If a tested component class declares an interceptor binding then you might need to mock the interception too.
228+
You can define a mock interceptor class as a static nested class of the test class.
229+
This interceptor class is then automatically discovered
230+
231+
[source, java]
232+
----
233+
import static org.junit.jupiter.api.Assertions.assertEquals;
234+
235+
import jakarta.inject.Inject;
236+
import io.quarkus.test.component.QuarkusComponentTest;
237+
import org.junit.jupiter.api.Test;
238+
239+
@QuarkusComponentTest
240+
public class FooTest {
241+
242+
@Inject
243+
Foo foo;
244+
245+
@Test
246+
public void testPing() {
247+
assertEquals("OK", foo.ping());
248+
}
249+
250+
@ApplicationScoped
251+
static class Foo {
252+
253+
@SimpleBinding <1>
254+
String ping() {
255+
return "ok";
256+
}
257+
258+
}
259+
260+
@SimpleBinding
261+
@Interceptor
262+
static class SimpleMockInterceptor { <2>
263+
264+
@AroundInvoke
265+
Object aroundInvoke(InvocationContext context) throws Exception {
266+
return context.proceed().toString().toUpperCase();
267+
}
268+
269+
}
270+
}
271+
----
272+
<1> `@SimpleBinding` is an interceptor binding.
273+
<2> The interceptor class is automatically considered a tested component and therefore used during the test execution.
274+
275+
== Summary
276+
277+
In this article, we discussed the possibilities of a new way of testing CDI components in a Quarkus application.

0 commit comments

Comments
 (0)