Skip to content

Commit 98b3b23

Browse files
committed
Blogpost: a component testing update
1 parent 82529d2 commit 98b3b23

File tree

1 file changed

+316
-0
lines changed

1 file changed

+316
-0
lines changed
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
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 we'll remind you of some newsworthy 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+
public class PersonService {
160+
161+
public List<Person> getPersons() {
162+
return Person.listAll();
163+
}
164+
}
165+
----
166+
167+
You can write a component test like:
168+
169+
[source, java]
170+
----
171+
import static org.junit.jupiter.api.Assertions.assertEquals;
172+
173+
import jakarta.inject.Inject;
174+
import io.quarkus.test.component.QuarkusComponentTest;
175+
import io.quarkus.panache.mock.MockPanacheEntities;
176+
import org.junit.jupiter.api.Test;
177+
import org.mockito.Mockito;
178+
179+
@QuarkusComponentTest <1>
180+
@MockPanacheEntities(Person.class) <2>
181+
public class PersonServiceTest {
182+
183+
@Inject
184+
PersonService personService; <3>
185+
186+
@Test
187+
public void testGetPersons() {
188+
Mockito.when(Person.listAll()).thenReturn(List.of(new Person("Tom")));
189+
List<Person> list = personService.getPersons();
190+
assertEquals(1, list.size());
191+
assertEquals("Tom", list.get(0).name);
192+
}
193+
194+
}
195+
----
196+
<1> The `QuarkusComponentTest` annotation registers the JUnit extension.
197+
<2> `@MockPanacheEntities` installs mocks for the given entity classes.
198+
<3> The test injects the component under the test - `PersonService`.
199+
200+
== Support `@InjectMock` for built-in `Event`
201+
202+
It is now possible to mock the built-in bean for `jakarta.enterprise.event.Event`.
203+
204+
Given this simple CDI bean:
205+
206+
[source,java]
207+
----
208+
import jakarta.enterprise.context.ApplicationScoped;
209+
import jakarta.enterprise.event.Event;
210+
import jakarta.inject.Inject;
211+
212+
@ApplicationScoped
213+
public class PersonService {
214+
215+
@Inject
216+
Event<Person> event;
217+
218+
void register(Person person) {
219+
event.fire(person);
220+
// ... business logic
221+
}
222+
}
223+
----
224+
225+
You can write a component test like:
226+
227+
[source, java]
228+
----
229+
import static org.junit.jupiter.api.Assertions.assertEquals;
230+
import static org.mockito.ArgumentMatchers.any;
231+
232+
import jakarta.inject.Inject;
233+
import io.quarkus.test.component.QuarkusComponentTest;
234+
import io.quarkus.test.InjectMock;
235+
import org.junit.jupiter.api.Test;
236+
import org.mockito.Mockito;
237+
238+
@QuarkusComponentTest <1>
239+
public class PersonServiceTest {
240+
241+
@Inject
242+
PersonService personService; <2>
243+
244+
@InjectMock
245+
Event<Person> event; <3>
246+
247+
@Test
248+
public void testRegister() {
249+
personService.register(new Person()); <4>
250+
Mockito.verify(event, Mockito.times(1)).fire(any()); <5>
251+
}
252+
253+
}
254+
----
255+
<1> The `QuarkusComponentTest` annotation registers the JUnit extension.
256+
<2> The test injects the component under the test - `PersonService`.
257+
<3> Install the mock for the built-in `Event`.
258+
<4> Call the `register()` method that should trigger an event.
259+
<5> Verify that the `Event#fire()` method was called exactly once.
260+
261+
=== Nested tests
262+
263+
JUnit `@Nested` tests may help to structure more complex test scenarios.
264+
However, its support has proven more troublesome than we expected.
265+
Still, we do support and test the basic use cases like this:
266+
267+
[source, java]
268+
----
269+
import static org.junit.jupiter.api.Assertions.assertEquals;
270+
271+
import jakarta.inject.Inject;
272+
import io.quarkus.test.InjectMock;
273+
import io.quarkus.test.component.TestConfigProperty;
274+
import io.quarkus.test.component.QuarkusComponentTest;
275+
import org.junit.jupiter.api.Test;
276+
import org.mockito.Mockito;
277+
278+
@QuarkusComponentTest <1>
279+
public class NestedTest {
280+
281+
@Inject
282+
Foo foo; <2>
283+
284+
@InjectMock
285+
Charlie charlieMock; <3>
286+
287+
@Nested
288+
class PingTest {
289+
290+
@Test
291+
public void testPing() {
292+
Mockito.when(charlieMock.ping()).thenReturn("OK");
293+
assertEquals("OK", foo.ping());
294+
}
295+
}
296+
297+
@Nested
298+
class PongTest {
299+
300+
@Test
301+
public void testPong() {
302+
Mockito.when(charlieMock.pong()).thenReturn("NOK");
303+
assertEquals("NOK", foo.pong());
304+
}
305+
}
306+
}
307+
----
308+
<1> The `QuarkusComponentTest` annotation registers the JUnit extension.
309+
<2> The test injects the component under the test. `Foo` injects `Charlie`.
310+
<3> The test also injects a mock for `Charlie`. The injected reference is an "unconfigured" Mockito mock.
311+
312+
== Conclusion
313+
314+
If you want to test the business logic of your components in isolation, with different configurations and inputs, then `QuarkusComponentTest` is a good choice.
315+
It's fast, integrated with continuous testing, and extensible.
316+
As always, we are looking forward to your feedback!

0 commit comments

Comments
 (0)