Mocking CDI beans in Quarkus
Testing Quarkus applications has been an important part of the Quarkus Developer Joy, which is why
@QuarkusTest
for testing JVM applications and @NativeTest
for black-box testing of the native images have been
part of Quarkus since the first release.
A recurring request however amongst our community members has been to have Quarkus allow them to selectively
mock certain CDI beans for specific tests. This post will introduce the new mocking capabilities
that 1.4
brings which aim to address those concerns, while also providing a glimpse of additional
improvements in this are that will be part of 1.5
.
Old approach
Let us assume that a Quarkus application contains the following (purely contrived) bean:
@ApplicationScoped
public class OrderService {
private final InvoiceService invoiceService;
private final InvoiceNotificationService invoiceNotificationService;
public OrderService(InvoiceService invoiceService, InvoiceNotificationService invoiceNotificationService) {
this.invoiceService = invoiceService;
this.invoiceNotificationService = invoiceNotificationService;
}
public Invoice generateSendInvoice(Long orderId) {
final Invoice invoice = invoiceService.generateForOrder(orderId);
if (invoice.isAlreadySent()) {
invoiceNotificationService.sendInvoice(invoice);
} else {
invoiceNotificationService.notifyInvoiceAlreadySent(invoice);
}
return invoice;
}
}
When testing the generateSendInvoice
method we most likely don’t want to use the actual InvoiceNotificationService
as it would
result in sending real notifications. With the old Quarkus approach one could "override"
the InvoiceNotificationService
in tests by adding the following bean in the test sources:
@Mock
public class MockInvoiceNotificationService implements InvoiceNotificationService {
public void sendInvoice(Invoice invoice) {
}
public void notifyInvoiceAlreadySent(Invoice invoice) {
}
}
When Quarkus scanned this code, the use of @Mock
would result in MockInvoiceNotificationService
being used as
the implementation of InvoiceNotificationService
in every place where a InvoiceNotificationService
bean was injected
(in CDI terms this is called an injection point).
Although this mechanism is fairly straightforward to use, it nonetheless suffers from a few problems:
-
A new class (or a new CDI producer method) needs to be used for each bean type that requires a mock. In a large application where a lot of mocks are needed, the amount of boilerplate code increases unacceptably.
-
There is no way for a mock to be used for certain tests only. This is due to the fact that beans that are annotated with
@Mock
are normal CDI beans (and are therefore used throughout the application). Depending on what needs to be tested, this can be very problematic. -
There is a no out of the box integration with Mockito, which is the de-facto standard for mocking in Java applications. Users can certainly use Mockito (most commonly by using a CDI producer method), but there is boilerplate code involved.
New approach
Starting with Quarkus 1.4
, users have the ability to create and inject per-test mocks for normal scoped CDI beans using
io.quarkus.test.junit.QuarkusMock
. Moreover, Quarkus provides out of the box integration with Mockito
allowing for zero effort mocking of CDI beans using the io.quarkus.test.junit.mockito.@InjectMock
annotation.
Using QuarkusMock
QuarkusMock
provides the foundation for mocking normal scoped CDI beans and is also used under the hood by @InjectMock
, so let us examine
it first. The best way to do this is using an example:
@QuarkusTest
public class MockTestCase {
@Inject
MockableBean1 mockableBean1;
@Inject
MockableBean2 mockableBean2;
@BeforeAll
public static void setup() {
MockableBean1 mock = Mockito.mock(MockableBean1.class); (1)
Mockito.when(mock.greet("Stuart")).thenReturn("A mock for Stuart");
QuarkusMock.installMockForType(mock, MockableBean1.class); (2)
}
@Test
public void testBeforeAll() {
Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart")); (3)
Assertions.assertEquals("Hello Stuart", mockableBean2.greet("Stuart")); (4)
}
@Test
public void testPerTestMock() {
QuarkusMock.installMockForInstance(new BonjourMockableBean2(), mockableBean2); (5)
Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart")); (6)
Assertions.assertEquals("Bonjour Stuart", mockableBean2.greet("Stuart")); (7)
}
@ApplicationScoped
public static class MockableBean1 {
public String greet(String name) {
return "Hello " + name;
}
}
@ApplicationScoped
public static class MockableBean2 {
public String greet(String name) {
return "Hello " + name;
}
}
public static class BonjourMockableBean2 extends MockableBean2 {
@Override
public String greet(String name) {
return "Bonjour " + name;
}
}
}
1 | This part of the example uses Mockito for convenience’s sake only. QuarkusMock is not tied to Mockito in any way. |
2 | We use QuarkusMock.installMockForType() because the injected bean instance is not yet available. Very important to note is that
the mock setup in a JUnit @BeforeAll method, is used for all test methods of the class (other test classes are not affected by this). |
3 | The mock for MockableBean1 is being used as it was defined for all test methods of the class. |
4 | Since no mock has been set up for MockableBean2 , the CDI bean is being used. |
5 | We use QuarkusMock.installMockForInstance() here because inside the test method, the injected bean instance is available. |
6 | The mock for MockableBean1 is being used as it was defined for all test methods of the class. |
7 | As we used BonjourMockableBean2 as a mock MockableBean2 , this class is now used. |
Furthermore, |
Returning to the original example of the blog post, we could get rid of the MockInvoiceNotificationService
class and
instead use something like the following:
public class OrderServiceTest {
@Inject
OrderService orderService;
@BeforeAll
public static void setup() {
MockableBean1 mock = Mockito.mock(InvoiceNotificationService.class);
Mockito.doNothing().when(mock).sendInvoice(any());
Mockito.doNothing().when(mock).notifyInvoiceAlreadySent(any());
QuarkusMock.installMockForType(mock, MockableBean1.class);
}
public void testGenerateSendInvoice() {
// perform some setup
Invoice invoice = orderService.generateSendInvoice(1L);
// perform some assertions
}
}
Note that in this case we don’t need to create a new class implementing InvoiceNotificationService
.
Moreover, we have full and per test control over the mock, something which grants up a lot of flexibility when writing tests.
For example, if we had some other test where we did want to use the real InvoiceNotificationService
, then in that test
we would simply not do any mocking of InvoiceNotificationService
.
If yet another test needed to mock InvoiceNotificationService
in some other way, then it would be perfectly free to do so,
using the same method OrderServiceTest
uses, without causing any problems to the other tests.
Finally, note in the example above we didn’t mock InvoiceService
, which meant that the real InvoiceService
was being used in OrderServiceTest
.
Using @InjectMock
Hopefully the previous section convinced you of the merits of QuarkusMock
over the old approach. You might also be wondering
however if there is a way to reduce boilerplate code even further and provide tighter integration with Mockito. That is
where @InjectMock
comes in handy.
To demonstrate @InjectMock
let’s rewrite the MockTestCase
from the previous section.
First of all, we need to add the following dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
Now we can rewrite the MockTestCase
like so:
@QuarkusTest
public class MockTestCase {
@InjectMock
MockableBean1 mockableBean1; (1)
@InjectMock
MockableBean2 mockableBean2;
@BeforeEach
public void setup() {
Mockito.when(mockableBean1.greet("Stuart")).thenReturn("A mock for Stuart"); (2)
}
@Test
public void firstTest() {
Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
Assertions.assertEquals(null, mockableBean2.greet("Stuart"));
}
@Test
public void secondTest() {
Mockito.when(mockableBean2.greet("Stuart")).thenReturn("Bonjour Stuart"); (3)
Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
Assertions.assertEquals("Bonjour Stuart", mockableBean2.greet("Stuart"));
}
@ApplicationScoped
public static class MockableBean1 {
public String greet(String name) {
return "Hello " + name;
}
}
@ApplicationScoped
public static class MockableBean2 {
public String greet(String name) {
return "Hello " + name;
}
}
}
1 | @InjectMock results in a mock being created and being available in all test methods of the test class (other test classes are not affected by this) |
2 | The mockableBean1 is configured here for all test methods of the class |
3 | Configure mockableBean2 for this test only |
Since Additionally, |
As a final example, we can rewrite the OrderServiceTest
test like so:
public class OrderServiceTest {
@Inject
private OrderService orderService;
@InjectMock
private InvoiceNotificationService invoiceNotificationService;
@BeforeAll
public static void setup() {
doNothing().when(invoiceNotificationService).sendInvoice(any());
doNothing().when(invoiceNotificationService).notifyInvoiceAlreadySent(any());
}
public void testGenerateSendInvoice() {
// perform some setup
Invoice invoice = orderService.generateSendInvoice(1L);
// perform some assertions
}
}
Using @InjectMock with @RestClient
A very common need is to mock @RestClient
beans. Thankfully it’s a need well covered by @InjectMock
- as long
as two principles are followed:
-
The bean is made
@ApplicationScoped
(instead of accepting the default scope which@RegisterRestClient
implies, i.e.@Dependent
) -
The
@RestClient
CDI qualifier is used when injecting the bean into the test.
As usual, an example best demonstrates these requirements. Say we have a GreetingService
which we wish to use to build a rest client:
@Path("/")
@ApplicationScoped (1)
@RegisterRestClient
public interface GreetingService {
@GET
@Path("/hello")
@Produces(MediaType.TEXT_PLAIN)
String hello();
}
1 | @ApplicationScoped needs to be used to make GreetingService mockable. |
An example test class could be:
@QuarkusTest
public class GreetingResourceTest {
@InjectMock
@RestClient (1)
GreetingService greetingService;
@Test
public void testHelloEndpoint() {
Mockito.when(greetingService.hello()).thenReturn("hello from mockito");
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("hello from mockito"));
}
}
1 | We need to use the @RestClient CDI qualifier, since Quarkus creates the GreetingService bean with this qualifier. |
More Mocking in Quarkus 1.5
Quarkus 1.5 will ship with a new testing module (quarkus-panache-mock
) that will make mocking Panache entities a breeze.
If you are eager to see what this feature is all about, check out this
and feel free to give us early feedback.