-
Notifications
You must be signed in to change notification settings - Fork 395
Blogpost: a component testing update #2429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
mkouba
wants to merge
1
commit into
quarkusio:main
Choose a base branch
from
mkouba:quarkus-component-test-update-2025
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
319 changes: 319 additions & 0 deletions
319
_posts/2025-10-20-quarkus-component-test-update.asciidoc
This file contains hidden or 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,319 @@ | ||
--- | ||
layout: post | ||
title: 'Quarkus - a component testing update' | ||
date: 2025-10-20 | ||
tags: testing | ||
synopsis: 'It has been a while since we introduced the component testing in Quarkus. What’s new? What new functionalities are available?' | ||
author: mkouba | ||
--- | ||
|
||
It's been a while since we https://quarkus.io/blog/quarkus-component-test/[introduced the component testing] in Quarkus. | ||
In this blogpost, we will first quickly summarize the basic principles and then describe some of the new interesting features. | ||
|
||
== Quick summary | ||
|
||
First, just a quick summary. | ||
The component model of Quarkus is built on top of CDI. | ||
An idiomatic way to test a Quarkus application is to use the `quarkus-junit5` module and `@QuarkusTest`. | ||
However, in this case, a full Quarkus application needs to be built and started. | ||
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. | ||
One of the consequences is that some components (typically `@ApplicationScoped` and `@Singleton` CDI beans) are shared as well. | ||
What if you need to test the business logic of a component in isolation, with different states and inputs? | ||
For this use case, a plain unit test would make a lot of sense. | ||
However, writing unit tests for CDI beans without a running CDI container is often a tedious work. | ||
Dependency injection, events, interceptors - all the work has to be done manually and everything needs to be wired together by hand. | ||
In Quarkus 3.2, we introduced an experimental feature to ease the testing of CDI components and mocking of their dependencies. | ||
It's a JUnit 5 extension that does not start a full Quarkus application but merely the CDI container and the Configuration service. | ||
|
||
=== The lifecycle | ||
|
||
So when exactly does the `QuarkusComponentTest` start the CDI container? | ||
It depends on the value of `@org.junit.jupiter.api.TestInstance#lifecycle`. | ||
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. | ||
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. | ||
|
||
=== Components under test | ||
|
||
When writing a component test, it's essential to understand how the set of _tested components_ is built. | ||
It's because the _tested components_ are treated as real beans, but all _unsatisfied dependencies_ are mocked automatically. | ||
What does it mean? | ||
Imagine that we have a bean `Foo` like this: | ||
|
||
[source,java] | ||
---- | ||
package org.example; | ||
|
||
import jakarta.enterprise.context.ApplicationScoped; | ||
import jakarta.inject.Inject; | ||
|
||
@ApplicationScoped | ||
public class Foo { | ||
|
||
@Inject | ||
Charlie charlie; | ||
|
||
public String ping() { | ||
return charlie.ping(); | ||
} | ||
} | ||
---- | ||
|
||
It has one dependency - a bean `Charlie`. | ||
Now if you want to write a unit test for `Foo` you need to make sure the `Charlie` dependency is injected and functional. | ||
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`. | ||
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: | ||
|
||
[source, java] | ||
---- | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
|
||
import jakarta.inject.Inject; | ||
import io.quarkus.test.InjectMock; | ||
import io.quarkus.test.component.QuarkusComponentTest; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.Mockito; | ||
|
||
@QuarkusComponentTest <1> | ||
public class FooTest { | ||
|
||
@Inject | ||
Foo foo; <2> | ||
|
||
@InjectMock | ||
Charlie charlieMock; <3> | ||
|
||
@Test | ||
public void testPing() { | ||
Mockito.when(charlieMock.ping()).thenReturn("OK"); <4> | ||
assertEquals("OK", foo.ping()); | ||
} | ||
} | ||
---- | ||
<1> The `QuarkusComponentTest` annotation registers the JUnit extension. | ||
<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. | ||
<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. | ||
<4> We can leverage the Mockito API in a test method to configure the behavior. | ||
|
||
The initial set of tested components is derived from the test class: | ||
|
||
1. First, the types of all fields annotated with `@jakarta.inject.Inject` are considered the component types. | ||
2. The types of test methods parameters that are not annotated with `@InjectMock`, `@SkipInject`, or `@org.mockito.Mock` are also considered the component types. | ||
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. | ||
|
||
Additional component classes can be set using `@QuarkusComponentTest#value()` or `QuarkusComponentTestExtensionBuilder#addComponentClasses()`. | ||
|
||
== What's new? | ||
|
||
. Quarkus 3.13 | ||
.. Removed the experimental status | ||
. Quarkus 3.21 | ||
.. Basic support for nested tests | ||
. Quarkus 3.29 | ||
.. Class loading refactoring | ||
.. `QuarkusComponentTestCallbacks` | ||
.. Integration with `quarkus-panache-mock` | ||
.. Support `@InjectMock` for built-in `Event` | ||
|
||
=== Class loading refactoring | ||
|
||
In the previous versions of `QuarkusComponentTest` it wasn't possible to perform bytecode transformations. | ||
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. | ||
That wasn't ideal because the tested CDI beans may have required changes before being used in a `QuarkusComponentTest`. | ||
This limitation is gone! | ||
The class loading is now more similar to a real Quarkus application. | ||
|
||
=== QuarkusComponentTestCallbacks | ||
|
||
We also introduced a new SPI - `QuarkusComponentTestCallbacks` - that can be used to contribute additional logic to the `QuarkusComponentTest` extension. | ||
There are several callbacks that can be used to modify the behavior before the container is built, after the container is started, etc. | ||
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. | ||
|
||
=== Integration with `quarkus-panache-mock` | ||
|
||
Thanks to class loading refactoring and `QuarkusComponentTestCallbacks` SPI, we're now able to do interesting stuff. | ||
Previously, whenever we got a question like: | ||
_"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. | ||
But it's no longer true. | ||
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]. | ||
|
||
Given this simple entity: | ||
|
||
[source,java] | ||
---- | ||
@Entity | ||
public class Person extends PanacheEntity { | ||
|
||
public String name; | ||
|
||
public Person(String name) { | ||
this.name = name; | ||
} | ||
|
||
} | ||
---- | ||
|
||
That is used in a simple bean: | ||
|
||
[source,java] | ||
---- | ||
import jakarta.enterprise.context.ApplicationScoped; | ||
|
||
@ApplicationScoped | ||
public class PersonService { | ||
|
||
public List<Person> getPersons() { | ||
return Person.listAll(); | ||
} | ||
} | ||
---- | ||
|
||
You can write a component test like: | ||
|
||
[source, java] | ||
---- | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
|
||
import jakarta.inject.Inject; | ||
import io.quarkus.test.component.QuarkusComponentTest; | ||
import io.quarkus.panache.mock.MockPanacheEntities; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.Mockito; | ||
|
||
@QuarkusComponentTest <1> | ||
@MockPanacheEntities(Person.class) <2> | ||
public class PersonServiceTest { | ||
|
||
@Inject | ||
PersonService personService; <3> | ||
|
||
@Test | ||
public void testGetPersons() { | ||
Mockito.when(Person.listAll()).thenReturn(List.of(new Person("Tom"))); | ||
List<Person> list = personService.getPersons(); | ||
assertEquals(1, list.size()); | ||
assertEquals("Tom", list.get(0).name); | ||
} | ||
|
||
} | ||
---- | ||
<1> The `QuarkusComponentTest` annotation registers the JUnit extension. | ||
<2> `@MockPanacheEntities` installs mocks for the given entity classes. | ||
<3> The test injects the component under the test - `PersonService`. | ||
|
||
=== Support `@InjectMock` for built-in `Event` | ||
|
||
It is now possible to mock the built-in bean for `jakarta.enterprise.event.Event`. | ||
|
||
Given this simple CDI bean: | ||
|
||
[source,java] | ||
---- | ||
import jakarta.enterprise.context.ApplicationScoped; | ||
import jakarta.enterprise.event.Event; | ||
import jakarta.inject.Inject; | ||
|
||
@ApplicationScoped | ||
public class PersonService { | ||
|
||
@Inject | ||
Event<Person> event; | ||
|
||
void register(Person person) { | ||
event.fire(person); | ||
// ... business logic | ||
} | ||
} | ||
---- | ||
|
||
You can write a component test like: | ||
|
||
[source, java] | ||
---- | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.mockito.ArgumentMatchers.any; | ||
|
||
import jakarta.inject.Inject; | ||
import io.quarkus.test.component.QuarkusComponentTest; | ||
import io.quarkus.test.InjectMock; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.Mockito; | ||
|
||
@QuarkusComponentTest <1> | ||
public class PersonServiceTest { | ||
|
||
@Inject | ||
PersonService personService; <2> | ||
|
||
@InjectMock | ||
Event<Person> event; <3> | ||
|
||
@Test | ||
public void testRegister() { | ||
personService.register(new Person()); <4> | ||
Mockito.verify(event, Mockito.times(1)).fire(any()); <5> | ||
} | ||
|
||
} | ||
---- | ||
<1> The `QuarkusComponentTest` annotation registers the JUnit extension. | ||
<2> The test injects the component under the test - `PersonService`. | ||
<3> Install the mock for the built-in `Event`. | ||
<4> Call the `register()` method that should trigger an event. | ||
<5> Verify that the `Event#fire()` method was called exactly once. | ||
|
||
=== Nested tests | ||
|
||
JUnit `@Nested` tests may help to structure more complex test scenarios. | ||
However, its support has proven more troublesome than we expected. | ||
Still, we do support and test the basic use cases like this: | ||
|
||
[source, java] | ||
---- | ||
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> | ||
public class NestedTest { | ||
|
||
@Inject | ||
Foo foo; <2> | ||
|
||
@InjectMock | ||
Charlie charlieMock; <3> | ||
|
||
@Nested | ||
class PingTest { | ||
|
||
@Test | ||
public void testPing() { | ||
Mockito.when(charlieMock.ping()).thenReturn("OK"); | ||
assertEquals("OK", foo.ping()); | ||
} | ||
} | ||
|
||
@Nested | ||
class PongTest { | ||
|
||
@Test | ||
public void testPong() { | ||
Mockito.when(charlieMock.pong()).thenReturn("NOK"); | ||
assertEquals("NOK", foo.pong()); | ||
} | ||
} | ||
} | ||
---- | ||
<1> The `QuarkusComponentTest` annotation registers the JUnit extension. | ||
<2> The test injects the component under the test. `Foo` injects `Charlie`. | ||
<3> The test also injects a mock for `Charlie`. The injected reference is an "unconfigured" Mockito mock. | ||
|
||
== Conclusion | ||
|
||
If you want to test the business logic of your components in isolation, with different configurations and inputs, then `QuarkusComponentTest` is a good choice. | ||
It's fast, integrated with continuous testing, and extensible. | ||
As always, we are looking forward to your feedback! |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.