Simple external resources Testing with Quarkus

This is the second tutorial about Quarkus Testing. In this article we will learn how to test Quarkus applications that require Integration tests with external resources such as Databases, Messaging Brokers, Identity Servers and more.

Overview of Quarkus Testing

Firstly, we recommend checking this tutorial to have an overview of Quarkus Testing: How to Test Quarkus applications

Within this article we will learn:

  • How to test Database applications
  • How to test other external Resources such as Kafka, AMQ, MongoDB, Keycloak.

Testing Database applications

In order to test against a Database, we will add a REST Endpoint which performs the standard Create, Read, Update, Delete operations:

@Path("/hero")
@Produces("application/json")
@Consumes("application/json")

public class HeroResource {

    @Inject
    EntityManager entityManager;

    @GET
    public Hero[] get() {
        return entityManager.createNamedQuery("Heros.findAll", Hero.class)
                .getResultList().toArray(new Hero[0]);
    }
    @POST
    @Transactional
    public Response create(Hero hero) {
        if (hero.getId() != null) {
            throw new WebApplicationException("Id was invalidly set on request.", 422);
        }

        entityManager.persist(hero);
        return Response.ok(hero).status(201).build();
    }

    @PUT
    @Transactional
    public Hero update(Hero hero) {
        if (hero.getId() == null) {
            throw new WebApplicationException("Hero Id was not set on request.", 422);
        }

        Hero entity = entityManager.find(Hero.class, hero.getId());

        if (entity == null) {
            throw new WebApplicationException("Hero with id of " + hero.getId() + " does not exist.", 404);
        }

        entity.setName(hero.getName());
        return entity;
    }

    @DELETE
    @Transactional
    public Response delete(Hero hero) {
        Hero entity = entityManager.find(Hero.class, hero.getId());
        if (entity == null) {
            throw new WebApplicationException("Hero with id of " + hero.getId() + " does not exist.", 404);
        }
        entityManager.remove(entity);
        return Response.status(204).build();
    }
 
}

Next, we will choose the target Database. For our example, we will be using PostgreSQL. Therefore we will include the following dependency in our pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>

Finally, let’s add a @QuarkusTest Class which tests the GET and POST methods:

@QuarkusTest
@TestHTTPEndpoint(HeroResource.class)

public class HeroEndpointTest {

    @Test
    public void testCustomerService() {

        RestAssured.given()
                .when().get()
                .then()
                .statusCode(200)
                .body("$.size()", is(3),
                        "[0].id", is(1),
                        "[0].name", is("Batman"),
                        "[1].id", is(2),
                        "[1].name", is("Superman"),
                        "[2].id", is(3),
                        "[2].name", is("Wonder woman")
                );

        given()
                .contentType("application/json")
                .body(new Hero("Iron man"))
                .when()
                .post()
                .then()
                .statusCode(201);


         RestAssured.given()
                .when().get()
                .then()
                .statusCode(200)
                .body("$.size()", is(4),
                        "[3].name", is("Iron man")
                );


    }
}

If you have read our first tutorial, you should be familiar with @QuarkusTest and @QuarkusHTTPEndpoint annotations.

We will then focus on how to test using PostgreSQL. The simplest way to do that, is to rely on the default image available for the Database. Quarkus Dev Services will automatically pull a Container Image for your Database when running Test or Dev mode.

To allow pulling the Container Image, it is sufficient to leave empty the datasource settings or, even better, set them just for the production profile. Example:

%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.username=quarkus_test
%prod.quarkus.datasource.password=quarkus_test
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost/quarkus_test
%prod.quarkus.datasource.jdbc.max-size=8
%prod.quarkus.datasource.jdbc.min-size=2

quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=import.sql

Now build the application:

mvn install

That will trigger the Tests available in the project. As you can see, the latest version of PostgreSQL Image was pulled and linked to our application:

On the other hand, if you want to use specific JDBC Settings for your Tests, just provide the quarkus.datasource properties for the default profile or, if you want to use them only for testing, for the test profile:

%test.quarkus.datasource.db-kind=postgresql
%test.quarkus.datasource.username=quarkus_test
%test.quarkus.datasource.password=quarkus_test
%test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost/quarkus_test
%test.quarkus.datasource.jdbc.max-size=8
%test.quarkus.datasource.jdbc.min-size=2

Finally, if you want to have a stricter control over the Container Image used by your application, we recommend 

Source code: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/testing

Rolling back Test Transactions

If your Test suite includes transactions, you can decorate your Test with the io.quarkus.test.TestTransaction annotation. This annotation will trigger the automatic rollback of all changes to the database when the method completes.

For example, the following method persists a new customer, verifies that the sequence id is not null. Then, when the method completes, the Customer insert will be rolled back:

@QuarkusTest
@TestTransaction
public class HeroRepositoryTests {

    @Inject
    HeroRepository repository;

    @Test
    void addCustomer() {
        Hero h= new Hero("Bruce","Banner");
        repository.persist(h);
        Assertions.assertNotNull(h.id);
    }
}

Testing other external resources

Besides Databases, Quarkus Dev Services allows to test also a variety of external resources by pulling automatically the Image for your tests. Here is a list of external resources which are capable of using Quarkus Dev Services:

In order to allow the automatic Image pulling, you have to follow a simple two-steps procedure:

Firstly, include the dependency for your external service. For example, if you were to Test a Kafka application, add the quarkus-smallrye-reactive-messaging-kafka extension is in your pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId>
</dependency>

Next, make sure that the Resource is not explicitly configured for the default/test profile. For example, to Test a Kafka application with an embedded Kafka Image, configure the bootstrap servers for the prod profile:

%prod.kafka.bootstrap.servers=localhost:9092

Finally, in order to build a @QuarkusTest which produces and consumes Messages with Kafka, you need the following resources:

  • A KafkaConsumer which you can bootstrap in the BeforeEach method of your Test
  • A KafkaProducer which you can also bootstrap in the BeforeEach method of the Test class

Here is a sample from Quarkus Kafka quickstart:

@QuarkusTest
public class QuoteProcessorTest {

    @Inject
    @Identifier("default-kafka-broker")
    Map<String, Object> kafkaConfig;

    KafkaProducer<String, String> quoteRequestProducer;
    KafkaConsumer<String, Quote> quoteConsumer;

    @BeforeEach
    void setUp() {
        quoteConsumer = new KafkaConsumer<>(consumerConfig(), new StringDeserializer(), new ObjectMapperDeserializer<>(Quote.class));
        quoteRequestProducer = new KafkaProducer<>(kafkaConfig, new StringSerializer(), new StringSerializer());
    }

    @AfterEach
    void tearDown() {
        quoteRequestProducer.close();
        quoteConsumer.close();
    }

    Properties consumerConfig() {
        Properties properties = new Properties();
        properties.putAll(kafkaConfig);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group-id");
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        return properties;
    }

    @Test
    void testProcessor() {
        quoteConsumer.subscribe(Collections.singleton("quotes"));
        UUID quoteId = UUID.randomUUID();
        quoteRequestProducer.send(new ProducerRecord<>("quote-requests", quoteId.toString()));
        ConsumerRecords<String, Quote> records = quoteConsumer.poll(Duration.ofMillis(10000));
        Quote quote = records.records("quotes").iterator().next().value();
        assertEquals(quote.id, quoteId.toString());
    }
}

Here is the log produced by Quarkus Dev Service which shows that you can connect to localhost:49172 to access your Test Kafka cluster from outside:

2022-05-09 16:59:26,026 INFO  [org.tes.DockerClientFactory] (build-17) Checking the system...
2022-05-09 16:59:26,027 INFO  [org.tes.DockerClientFactory] (build-17) ︎ Docker server version should be at least 1.6.0
2022-05-09 16:59:26,090 INFO  [org.tes.DockerClientFactory] (build-17) ︎ Docker environment should have more than 2GB free disk space
2022-05-09 16:59:26,148 INFO  [ .io/.11.2]] (build-17) Creating container for image: docker.io/vectorized/redpanda:v21.11.2
2022-05-09 16:59:26,321 INFO  [ .io/.11.2]] (build-17) Container docker.io/vectorized/redpanda:v21.11.2 is starting: 7c7e743d15dfa4cbc75c2ac2e1ca202a3fc5478aea0061bb0fcb0337a0d5c982
2022-05-09 16:59:27,551 INFO  [ .io/.11.2]] (build-17) Container docker.io/vectorized/redpanda:v21.11.2 started in PT1.449898S
2022-05-09 16:59:27,552 INFO  [io.qua.kaf.cli.dep.DevServicesKafkaProcessor] (build-17) Dev Services for Kafka started. Other Quarkus applications in dev mode will find the broker automatically. For Quarkus applications in production mode, you can connect to this by starting your application with -Dkafka.bootstrap.servers=OUTSIDE://localhost:49172

Testing Keycloak

Let’s see another example. To Test Keycloak OIDC authorization include the following dependencies in your pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-keycloak-authorization</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc</artifactId>
</dependency>

Next, to allow Quarkus Test to pull Keycloak Image, make sure the property quarkus.oidc.auth-server-url is undefined for the dev/test profile:

%prod.quarkus.oidc.auth-server-url=https://localhost:8543/realms/quarkus
quarkus.oidc.client-id=backend-service
quarkus.oidc.credentials.secret=secret
quarkus.oidc.tls.verification=none
quarkus.keycloak.devservices.realm-path=quarkus-realm.json
quarkus.oidc.token.issuer=any

# Enable Policy Enforcement
quarkus.keycloak.policy-enforcer.enable=true
quarkus.keycloak.policy-enforcer.lazy-load-paths=false

# Disables policy enforcement for a path
quarkus.keycloak.policy-enforcer.paths.1.path=/api/public
quarkus.keycloak.policy-enforcer.paths.1.enforcement-mode=DISABLED

Finally, the following Test Class shows how to use io.quarkus.test.keycloak.client.KeycloakTestClient to connect to the Keycloak Identity Server:

@QuarkusTest
public class PolicyEnforcerTest {
    static {
        RestAssured.useRelaxedHTTPSValidation();
    }

    KeycloakTestClient keycloakClient = new KeycloakTestClient();

    @Test
    public void testAccessUserResource() {
        RestAssured.given().auth().oauth2(getAccessToken("alice"))
                .when().get("/api/users/me")
                .then()
                .statusCode(200);
        RestAssured.given().auth().oauth2(getAccessToken("jdoe"))
                .when().get("/api/users/me")
                .then()
                .statusCode(200);
    }

    @Test
    public void testAccessAdminResource() {
        RestAssured.given().auth().oauth2(getAccessToken("alice"))
                .when().get("/api/admin")
                .then()
                .statusCode(403);
        RestAssured.given().auth().oauth2(getAccessToken("jdoe"))
                .when().get("/api/admin")
                .then()
                .statusCode(403);
        RestAssured.given().auth().oauth2(getAccessToken("admin"))
                .when().get("/api/admin")
                .then()
                .statusCode(200);
    }

    @Test
    public void testPublicResource() {
        RestAssured.given()
                .when().get("/api/public")
                .then()
                .statusCode(204);
    }

    private String getAccessToken(String userName) {
        return keycloakClient.getAccessToken(userName);
    }
}

Conclusion

In this second tutorial about Quarkus Testing, we have covered Integration testing with external resources. We have shown how to use Quarkus Dev Services to simplify the testing how external resources by automatically pulling container images.