diff --git a/_posts/2025-10-20-quarkus-component-test-update.asciidoc b/_posts/2025-10-20-quarkus-component-test-update.asciidoc new file mode 100644 index 0000000000..c9ffd429f7 --- /dev/null +++ b/_posts/2025-10-20-quarkus-component-test-update.asciidoc @@ -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 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 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 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 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! \ No newline at end of file