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.

QuarkusMock can be used for any normal scoped CDI bean - the most common of which are @ApplicationScoped and @RequestScoped. This means that beans with @Singleton and @Dependent scope cannot be used with QuarkusMock.

Furthermore, QuarkusMock will not work properly when it’s used in tests that run parallel in the same JVM.

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 @InjectMock uses QuarkusMock under the hood, the same limitations apply to its use.

Additionally, @InjectMock works like an injection point for the bean, so for it to work properly when the target bean uses CDI qualifiers, those qualifiers also need to be added to the field. We will see an example of this in next section about mocking a @RestClient bean.

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.