Skip to content

Commit 96282c0

Browse files
authored
Merge pull request #2429 from mkouba/quarkus-component-test-update-2025
Blogpost: a component testing update
2 parents 8ba2c76 + 7cba1c4 commit 96282c0

File tree

1 file changed

+319
-0
lines changed

1 file changed

+319
-0
lines changed
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
---
2+
layout: post
3+
title: 'Quarkus - a component testing update'
4+
date: 2025-10-20
5+
tags: testing
6+
synopsis: 'It has been a while since we introduced the component testing in Quarkus. What’s new? What new functionalities are available?'
7+
author: mkouba
8+
---
9+
10+
It's been a while since we https://quarkus.io/blog/quarkus-component-test/[introduced the component testing] in Quarkus.
11+
In this blogpost, we will first quickly summarize the basic principles and then describe some of the new interesting features.
12+
13+
== Quick summary
14+
15+
First, just a quick summary.
16+
The component model of Quarkus is built on top of CDI.
17+
An idiomatic way to test a Quarkus application is to use the `quarkus-junit5` module and `@QuarkusTest`.
18+
However, in this case, a full Quarkus application needs to be built and started.
19+
In order to avoid unnecessary rebuilds and restarts the application is shared for multiple tests, unless a https://quarkus.io/guides/getting-started-testing#testing_different_profiles[different test profile] is used.
20+
One of the consequences is that some components (typically `@ApplicationScoped` and `@Singleton` CDI beans) are shared as well.
21+
What if you need to test the business logic of a component in isolation, with different states and inputs?
22+
For this use case, a plain unit test would make a lot of sense.
23+
However, writing unit tests for CDI beans without a running CDI container is often a tedious work.
24+
Dependency injection, events, interceptors - all the work has to be done manually and everything needs to be wired together by hand.
25+
In Quarkus 3.2, we introduced an experimental feature to ease the testing of CDI components and mocking of their dependencies.
26+
It's a JUnit 5 extension that does not start a full Quarkus application but merely the CDI container and the Configuration service.
27+
28+
=== The lifecycle
29+
30+
So when exactly does the `QuarkusComponentTest` start the CDI container?
31+
It depends on the value of `@org.junit.jupiter.api.TestInstance#lifecycle`.
32+
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.
33+
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.
34+
35+
=== Components under test
36+
37+
When writing a component test, it's essential to understand how the set of _tested components_ is built.
38+
It's because the _tested components_ are treated as real beans, but all _unsatisfied dependencies_ are mocked automatically.
39+
What does it mean?
40+
Imagine that we have a bean `Foo` like this:
41+
42+
[source,java]
43+
----
44+
package org.example;
45+
46+
import jakarta.enterprise.context.ApplicationScoped;
47+
import jakarta.inject.Inject;
48+
49+
@ApplicationScoped
50+
public class Foo {
51+
52+
@Inject
53+
Charlie charlie;
54+
55+
public String ping() {
56+
return charlie.ping();
57+
}
58+
}
59+
----
60+
61+
It has one dependency - a bean `Charlie`.
62+
Now if you want to write a unit test for `Foo` you need to make sure the `Charlie` dependency is injected and functional.
63+
In `QuarkusComponentTest`, if you include `Foo` in the set of tested components but `Charlie` is not included, then a mock is automatically injected into `Foo.charlie`.
64+
What's also important is that you can inject the mock directly in the test using the `@InjectMock` annotation and configure the mock in a test method:
65+
66+
[source, java]
67+
----
68+
import static org.junit.jupiter.api.Assertions.assertEquals;
69+
70+
import jakarta.inject.Inject;
71+
import io.quarkus.test.InjectMock;
72+
import io.quarkus.test.component.QuarkusComponentTest;
73+
import org.junit.jupiter.api.Test;
74+
import org.mockito.Mockito;
75+
76+
@QuarkusComponentTest <1>
77+
public class FooTest {
78+
79+
@Inject
80+
Foo foo; <2>
81+
82+
@InjectMock
83+
Charlie charlieMock; <3>
84+
85+
@Test
86+
public void testPing() {
87+
Mockito.when(charlieMock.ping()).thenReturn("OK"); <4>
88+
assertEquals("OK", foo.ping());
89+
}
90+
}
91+
----
92+
<1> The `QuarkusComponentTest` annotation registers the JUnit extension.
93+
<2> The test injects `Foo` - it's included in the set of tested components. In other words, it's treated as a real CDI bean.
94+
<3> 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.
95+
<4> We can leverage the Mockito API in a test method to configure the behavior.
96+
97+
The initial set of tested components is derived from the test class:
98+
99+
1. First, the types of all fields annotated with `@jakarta.inject.Inject` are considered the component types.
100+
2. The types of test methods parameters that are not annotated with `@InjectMock`, `@SkipInject`, or `@org.mockito.Mock` are also considered the component types.
101+
3. Finally, if `@QuarkusComponentTest#addNestedClassesAsComponents()` is set to `true` (it is by default) then all static nested classes declared on the test class are components too.
102+
103+
Additional component classes can be set using `@QuarkusComponentTest#value()` or `QuarkusComponentTestExtensionBuilder#addComponentClasses()`.
104+
105+
== What's new?
106+
107+
. Quarkus 3.13
108+
.. Removed the experimental status
109+
. Quarkus 3.21
110+
.. Basic support for nested tests
111+
. Quarkus 3.29
112+
.. Class loading refactoring
113+
.. `QuarkusComponentTestCallbacks`
114+
.. Integration with `quarkus-panache-mock`
115+
.. Support `@InjectMock` for built-in `Event`
116+
117+
=== Class loading refactoring
118+
119+
In the previous versions of `QuarkusComponentTest` it wasn't possible to perform bytecode transformations.
120+
As a result, features like https://quarkus.io/guides/cdi-reference#simplified-constructor-injection[simplified constructor injection] or ability to https://quarkus.io/guides/cdi-reference#unproxyable_classes_transformation[handle final classes and methods] were not supported.
121+
That wasn't ideal because the tested CDI beans may have required changes before being used in a `QuarkusComponentTest`.
122+
This limitation is gone!
123+
The class loading is now more similar to a real Quarkus application.
124+
125+
=== QuarkusComponentTestCallbacks
126+
127+
We also introduced a new SPI - `QuarkusComponentTestCallbacks` - that can be used to contribute additional logic to the `QuarkusComponentTest` extension.
128+
There are several callbacks that can be used to modify the behavior before the container is built, after the container is started, etc.
129+
It is a service provider, so all you have to do is to create a file located in `META-INF/services/io.quarkus.test.component.QuarkusComponentTestCallbacks` that contains the fully qualified name of your implementation class.
130+
131+
=== Integration with `quarkus-panache-mock`
132+
133+
Thanks to class loading refactoring and `QuarkusComponentTestCallbacks` SPI, we're now able to do interesting stuff.
134+
Previously, whenever we got a question like:
135+
_"What if I use Panache entities with the active record pattern? How I do write a test for a component that is using such entities?"_, we had to admit that it wasn't possible.
136+
But it's no longer true.
137+
Once you add the `quarkus-panache-mock` module in your application you can write the component test in a similar way as with the https://quarkus.io/guides/hibernate-orm-panache#using-the-active-record-pattern[`PanacheMock` API].
138+
139+
Given this simple entity:
140+
141+
[source,java]
142+
----
143+
@Entity
144+
public class Person extends PanacheEntity {
145+
146+
public String name;
147+
148+
public Person(String name) {
149+
this.name = name;
150+
}
151+
152+
}
153+
----
154+
155+
That is used in a simple bean:
156+
157+
[source,java]
158+
----
159+
import jakarta.enterprise.context.ApplicationScoped;
160+
161+
@ApplicationScoped
162+
public class PersonService {
163+
164+
public List<Person> getPersons() {
165+
return Person.listAll();
166+
}
167+
}
168+
----
169+
170+
You can write a component test like:
171+
172+
[source, java]
173+
----
174+
import static org.junit.jupiter.api.Assertions.assertEquals;
175+
176+
import jakarta.inject.Inject;
177+
import io.quarkus.test.component.QuarkusComponentTest;
178+
import io.quarkus.panache.mock.MockPanacheEntities;
179+
import org.junit.jupiter.api.Test;
180+
import org.mockito.Mockito;
181+
182+
@QuarkusComponentTest <1>
183+
@MockPanacheEntities(Person.class) <2>
184+
public class PersonServiceTest {
185+
186+
@Inject
187+
PersonService personService; <3>
188+
189+
@Test
190+
public void testGetPersons() {
191+
Mockito.when(Person.listAll()).thenReturn(List.of(new Person("Tom")));
192+
List<Person> list = personService.getPersons();
193+
assertEquals(1, list.size());
194+
assertEquals("Tom", list.get(0).name);
195+
}
196+
197+
}
198+
----
199+
<1> The `QuarkusComponentTest` annotation registers the JUnit extension.
200+
<2> `@MockPanacheEntities` installs mocks for the given entity classes.
201+
<3> The test injects the component under the test - `PersonService`.
202+
203+
=== Support `@InjectMock` for built-in `Event`
204+
205+
It is now possible to mock the built-in bean for `jakarta.enterprise.event.Event`.
206+
207+
Given this simple CDI bean:
208+
209+
[source,java]
210+
----
211+
import jakarta.enterprise.context.ApplicationScoped;
212+
import jakarta.enterprise.event.Event;
213+
import jakarta.inject.Inject;
214+
215+
@ApplicationScoped
216+
public class PersonService {
217+
218+
@Inject
219+
Event<Person> event;
220+
221+
void register(Person person) {
222+
event.fire(person);
223+
// ... business logic
224+
}
225+
}
226+
----
227+
228+
You can write a component test like:
229+
230+
[source, java]
231+
----
232+
import static org.junit.jupiter.api.Assertions.assertEquals;
233+
import static org.mockito.ArgumentMatchers.any;
234+
235+
import jakarta.inject.Inject;
236+
import io.quarkus.test.component.QuarkusComponentTest;
237+
import io.quarkus.test.InjectMock;
238+
import org.junit.jupiter.api.Test;
239+
import org.mockito.Mockito;
240+
241+
@QuarkusComponentTest <1>
242+
public class PersonServiceTest {
243+
244+
@Inject
245+
PersonService personService; <2>
246+
247+
@InjectMock
248+
Event<Person> event; <3>
249+
250+
@Test
251+
public void testRegister() {
252+
personService.register(new Person()); <4>
253+
Mockito.verify(event, Mockito.times(1)).fire(any()); <5>
254+
}
255+
256+
}
257+
----
258+
<1> The `QuarkusComponentTest` annotation registers the JUnit extension.
259+
<2> The test injects the component under the test - `PersonService`.
260+
<3> Install the mock for the built-in `Event`.
261+
<4> Call the `register()` method that should trigger an event.
262+
<5> Verify that the `Event#fire()` method was called exactly once.
263+
264+
=== Nested tests
265+
266+
JUnit `@Nested` tests may help to structure more complex test scenarios.
267+
However, its support has proven more troublesome than we expected.
268+
Still, we do support and test the basic use cases like this:
269+
270+
[source, java]
271+
----
272+
import static org.junit.jupiter.api.Assertions.assertEquals;
273+
274+
import jakarta.inject.Inject;
275+
import io.quarkus.test.InjectMock;
276+
import io.quarkus.test.component.TestConfigProperty;
277+
import io.quarkus.test.component.QuarkusComponentTest;
278+
import org.junit.jupiter.api.Test;
279+
import org.mockito.Mockito;
280+
281+
@QuarkusComponentTest <1>
282+
public class NestedTest {
283+
284+
@Inject
285+
Foo foo; <2>
286+
287+
@InjectMock
288+
Charlie charlieMock; <3>
289+
290+
@Nested
291+
class PingTest {
292+
293+
@Test
294+
public void testPing() {
295+
Mockito.when(charlieMock.ping()).thenReturn("OK");
296+
assertEquals("OK", foo.ping());
297+
}
298+
}
299+
300+
@Nested
301+
class PongTest {
302+
303+
@Test
304+
public void testPong() {
305+
Mockito.when(charlieMock.pong()).thenReturn("NOK");
306+
assertEquals("NOK", foo.pong());
307+
}
308+
}
309+
}
310+
----
311+
<1> The `QuarkusComponentTest` annotation registers the JUnit extension.
312+
<2> The test injects the component under the test. `Foo` injects `Charlie`.
313+
<3> The test also injects a mock for `Charlie`. The injected reference is an "unconfigured" Mockito mock.
314+
315+
== Conclusion
316+
317+
If you want to test the business logic of your components in isolation, with different configurations and inputs, then `QuarkusComponentTest` is a good choice.
318+
It's fast, integrated with continuous testing, and extensible.
319+
As always, we are looking forward to your feedback!

0 commit comments

Comments
 (0)