Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

Quarkus makes it very easy these days to develop robust and clean applications. But how about testing?

In this tutorial, we’ll take a close look at how a Quarkus application can be tested. We’ll explore the testing possibilities offered by Quarkus and present concepts like dependency management and injection, mocking, profile configuration, and more specific things like Quarkus annotations and testing a native executable.

2. Setup

Let’s start with the basic Quarkus project configured in our previous Guide to QuarkusIO.

First, we’ll add the quarkus-reasteasy-jackson, quarkus-hibernate-orm-panache, quarkus-jdbc-h2, quarkus-junit5-mockito, and quarkus-test-h2 Maven dependencies:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-h2</artifactId>
</dependency>

Next, let’s create our domain entity:

public class Book extends PanacheEntity {
    private String title;
    private String author;
}

We continue by adding a simple Panache repository with a method to search for books:

public class BookRepository implements PanacheRepository {

    public Stream<Book> findBy(String query) {
        return find("author like :query or title like :query", with("query", "%"+query+"%")).stream();
    }
}

Now, let’s write a LibraryService to hold any business logic:

public class LibraryService {

    public Set<Book> find(String query) {
        if (query == null) {
            return bookRepository.findAll().stream().collect(toSet());
        }
        return bookRepository.findBy(query).collect(toSet());
    }
}

And finally, let’s expose our service functionality through HTTP by creating a LibraryResource:

@Path("/library")
public class LibraryResource {

    @GET
    @Path("/book")
    public Set findBooks(@QueryParam("query") String query) {
        return libraryService.find(query);
    }
}

3. @Alternative Implementations

Before writing any tests, let’s ensure we have some books in our repository. With Quarkus, we can use the CDI @Alternative mechanism to provide a custom bean implementation for our tests. Let’s create a TestBookRepository that extends BookRepository:

@Priority(1)
@Alternative
@ApplicationScoped
public class TestBookRepository extends BookRepository {

    @PostConstruct
    public void init() {
        persist(new Book("Dune", "Frank Herbert"),
          new Book("Foundation", "Isaac Asimov"));
    }

}

We place this alternative bean in our test package, and because of the @Priority(1) and @Alternative annotations, we’re sure any test will pick it up over the actual BookRepository implementation. This is one way to provide a global mock that all our Quarkus tests can use. We’ll explore more narrow-focused mocks shortly, but now, let’s move on to creating our first test.

4. HTTP Integration Test

Let’s begin by creating a simple REST-assured integration test:

@QuarkusTest
class LibraryResourceIntegrationTest {

    @Test
    void whenGetBooksByTitle_thenBookShouldBeFound() {

        given().contentType(ContentType.JSON).param("query", "Dune")
          .when().get("/library/book")
          .then().statusCode(200)
          .body("size()", is(1))
          .body("title", hasItem("Dune"))
          .body("author", hasItem("Frank Herbert"));
    }
}

This test, annotated with @QuarkusTest, first starts the Quarkus application and then performs a series of HTTP requests against our resource’s endpoint.

Now, let’s make use of some Quarkus mechanisms to try and further improve our test.

4.1. URL Injection With @TestHTTPResource

Instead of hard-coding the path of our HTTP endpoint, let’s inject the resource URL:

@TestHTTPResource("/library/book")
URL libraryEndpoint;

And then, let’s use it in our requests:

given().param("query", "Dune")
  .when().get(libraryEndpoint)
  .then().statusCode(200);

Or, without using Rest-assured, let’s simply open a connection to the injected URL and test the response:

@Test
void whenGetBooks_thenBooksShouldBeFound() throws IOException {
    assertTrue(IOUtils.toString(libraryEndpoint.openStream(), defaultCharset()).contains("Asimov"));
}

As we can see, @TestHTTPResource URL injection gives us an easy and flexible way of accessing our endpoint.

4.2. @TestHTTPEndpoint

Let’s take this further and configure our endpoint using the Quarkus provided @TestHTTPEndpoint annotation:

@TestHTTPEndpoint(LibraryResource.class)
@TestHTTPResource("book")
URL libraryEndpoint;

This way, if we ever decide to change the path of the LibraryResource, the test will pick up the correct path without us having to touch it.

@TestHTTPEndpoint can also be applied at the class level, in which case REST-assured will automatically prefix all requests with the Path of the LibraryResource:

@QuarkusTest
@TestHTTPEndpoint(LibraryResource.class)
class LibraryHttpEndpointIntegrationTest {

    @Test
    void whenGetBooks_thenShouldReturnSuccessfully() {
        given().contentType(ContentType.JSON)
          .when().get("book")
          .then().statusCode(200);
    }
}

5. Context and Dependency Injection

When it comes to dependency injection, in Quarkus tests, we can use @Inject for any required dependency. Let’s see this in action by creating a test for our LibraryService:

@QuarkusTest
class LibraryServiceIntegrationTest {

    @Inject
    LibraryService libraryService;

    @Test
    void whenFindByAuthor_thenBookShouldBeFound() {
        assertFalse(libraryService.find("Frank Herbert").isEmpty());
    }
}

Now, let’s try to test our Panache BookRepository:

class BookRepositoryIntegrationTest {

    @Inject
    BookRepository bookRepository;

    @Test
    void givenBookInRepository_whenFindByAuthor_thenShouldReturnBookFromRepository() {
        assertTrue(bookRepository.findBy("Herbert").findAny().isPresent());
    }
}

But when we ran our test, it failed. That’s because it requires running within the context of a transaction, and none is active. This can be fixed simply by adding @Transactional to the test class. Or, if we prefer, we can define our own stereotype to bundle both @QuarkusTest and @Transactional. Let’s do this by creating the @QuarkusTransactionalTest annotation:

@QuarkusTest
@Stereotype
@Transactional
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface QuarkusTransactionalTest {
}

Now, let’s apply it to our test:

@QuarkusTransactionalTest
class BookRepositoryIntegrationTest

As we can see, because Quarkus tests are full CDI beans, we can take advantage of all the CDI benefits like dependency injection, transactional contexts, and CDI interceptors.

6. Mocking

Mocking is a critical aspect of any testing effort. As we’ve already seen above, Quarkus tests can make use of the CDI @Alternative mechanism. Let’s now dive deeper into the mocking capabilities Quarkus has to offer.

6.1. @Mock

As a slight simplification of the @Alternative approach, we can use the @Mock stereotype annotation. This bundles together the @Alternative and @Primary(1) annotations.

6.2. @QuarkusMock

If we don’t want to have a globally defined mock but would rather have our mock only within the scope of one test, we can use @QuarkusMock:

@QuarkusTest
class LibraryServiceQuarkusMockUnitTest {

    @Inject
    LibraryService libraryService;

    @BeforeEach
    void setUp() {
        BookRepository mock = Mockito.mock(TestBookRepository.class);
        Mockito.when(mock.findBy("Asimov"))
          .thenReturn(Arrays.stream(new Book[] {
            new Book("Foundation", "Isaac Asimov"),
            new Book("I Robot", "Isaac Asimov")}));
        QuarkusMock.installMockForType(mock, BookRepository.class);
    }

    @Test
    void whenFindByAuthor_thenBooksShouldBeFound() {
        assertEquals(2, libraryService.find("Asimov").size());
    }
}

6.3. @InjectMock

Let’s simplify things a bit and use the Quarkus @InjectMock annotation instead of @QuarkusMock:

@QuarkusTest
class LibraryServiceInjectMockUnitTest {

    @Inject
    LibraryService libraryService;

    @InjectMock
    BookRepository bookRepository;

    @BeforeEach
    void setUp() {
        when(bookRepository.findBy("Frank Herbert"))
          .thenReturn(Arrays.stream(new Book[] {
            new Book("Dune", "Frank Herbert"),
            new Book("Children of Dune", "Frank Herbert")}));
    }

    @Test
    void whenFindByAuthor_thenBooksShouldBeFound() {
        assertEquals(2, libraryService.find("Frank Herbert").size());
    }
}

6.4. @InjectSpy

If we’re only interested in spying and not replacing bean behaviour, we can use the provided @InjectSpy annotation:

@QuarkusTest
class LibraryResourceInjectSpyIntegrationTest {

    @InjectSpy
    LibraryService libraryService;

    @Test
    void whenGetBooksByAuthor_thenBookShouldBeFound() {
        given().contentType(ContentType.JSON).param("query", "Asimov")
          .when().get("/library/book")
          .then().statusCode(200);

        verify(libraryService).find("Asimov");
    }

}

7. Test Profiles

We might want to run our tests in different configurations. For this, Quarkus offers the concept of a test profile. Let’s create a test that runs against a different database engine using a customized version of our BookRepository, and that will also expose our HTTP resources at a different path from the one already configured.

For this, we start by implementing a QuarkusTestProfile:

public class CustomTestProfile implements QuarkusTestProfile {

    @Override
    public Map<String, String> getConfigOverrides() {
        return Collections.singletonMap("quarkus.resteasy.path", "/custom");
    }

    @Override
    public Set<Class<?>> getEnabledAlternatives() {
        return Collections.singleton(TestBookRepository.class);
    }

    @Override
    public String getConfigProfile() {
        return "custom-profile";
    }
}

Let’s now configure our application.properties by adding a custom-profile config property that will change our H2 storage from memory to file:

%custom-profile.quarkus.datasource.jdbc.url = jdbc:h2:file:./testdb

Finally, with all the resources and configuration in place, let’s write our test:

@QuarkusTest
@TestProfile(CustomBookRepositoryProfile.class)
class CustomLibraryResourceManualTest {

    public static final String BOOKSTORE_ENDPOINT = "/custom/library/book";

    @Test
    void whenGetBooksGivenNoQuery_thenAllBooksShouldBeReturned() {
        given().contentType(ContentType.JSON)
          .when().get(BOOKSTORE_ENDPOINT)
          .then().statusCode(200)
          .body("size()", is(2))
          .body("title", hasItems("Foundation", "Dune"));
    }
}

As we can see from the @TestProfile annotation, this test will use the CustomTestProfile. It will make HTTP requests to the custom endpoint overridden in the profile’s getConfigOverrides method. Moreover, it will use the alternative book repository implementation configured in the getEnabledAlternatives method. And finally, by using the custom-profile defined in getConfigProfile, it will persist data in a file rather than memory.

One thing to note is that Quarkus will shut down and then restart with the new profile before this test is executed. This adds some time as the shutdown/restart happens, but it’s the price to be paid for the extra flexibility.

8. Testing Native Executables

Quarkus offers the possibility to test native executables. Let’s create a native image test:

@QuarkusIntegrationTest
@QuarkusTestResource(H2DatabaseTestResource.class)
class NativeLibraryResourceIT extends LibraryHttpEndpointIntegrationTest {
}

And now, by running:

mvn verify -Pnative

We’ll see the native image being built and the tests running against it.

The @QuarkusIntegrationTest annotation instructs Quarkus to run this test against the native image, while the @QuarkusTestResource will start an H2 instance into a separate process before the test begins. The latter is needed for running tests against native executables, as the database engine is not embedded into the native image.

The @QuarkusTestResource annotation can also be used to start custom services, like TestContainers, for example. All we need to do is implement the QuarkusTestResourceLifecycleManager interface and annotate our test with:

@QuarkusTestResource(OurCustomResourceImpl.class)

You will need a GraalVM to build the native image.

Also, take notice that, at the moment, the injection does not work with native image testing. The only thing that runs natively is the Quarkus application, not the test itself.

9. Conclusion

In this article, we saw how Quarkus offers excellent support for testing our application. From simple things like dependency management, injection, and mocking to more complex aspects like configuration profiles and native images, Quarkus provides us with many tools to create powerful and clean tests.

As always, the complete code is available on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – Microservices (eBook) (cat=Cloud/Spring Cloud)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.