BT

InfoQ Homepage Articles Testing Quarkus Web Applications: Writing Clean Component Tests

Testing Quarkus Web Applications: Writing Clean Component Tests

Lire ce contenu en français

Bookmarks

Key Takeaways

  • Quarkus is a full-stack, Kubernetes-native Java framework made for Java virtual machines (JVMs) and native compilation. Although many testing techniques remain the same, Quarkus provides supporting technologies to ease test set up and execution.
  • In this article, we will learn how to write clean integration tests for Quarkus applications. We will see how we can write simple and clean tests for the following scenarios: a mail client, security with RBAC, testing using containers, and rest clients.
  • Quarkus has a Quarkus Test Security module that allow deterministic modification of the security context during the test phase.
  • Testcontainers is a Java library that provides a way to start/stop Docker containers programmatically from the Java code. I supports out-of-the-box most used database containers, Kafka, Localstack, or WebDrivers to name a few.
  • Service virtualization is a technique used to simulate the behavior of dependencies of a service. Although service virtualization is commonly associated with REST API-based services, the same concept can be applied to any other kind of dependencies such as databases, ESBs, and JMS.

 

Quarkus is a full-stack, Kubernetes-native Java framework made for Java virtual machines (JVMs) and native compilation, optimizing Java specifically for containers and enabling it to become an effective platform for serverless, cloud, and Kubernetes environments.

Instead of reinventing the wheel, Quarkus uses well-known enterprise-grade frameworks backed by standards/specifications and makes them compilable to a binary using Graal VM.

In this article, we will learn how to write clean integration tests for Quarkus applications. We will see how we can write simple and clean tests for the following scenarios:

  • Mail Client
  • Security with RBAC
  • Testing using Containers
  • Rest Clients

Let’s see how we can write tests.

The application

We’ll use the same application from part 1 of this article located here.

As a reminder, the application is a simple user registration service developed in Quarkus, and it’s composed of the following classes:

Mail Client

Let’s add a new requirement when a new user is registered. Suppose every time a new user is registered, the application should send an email to the user with the auto-generated password. The logic to implement this use case uses the quarkus-mailer extension and it’s implemented in a CDI bean:

import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer;

@ApplicationScoped
public class MailService {
  
   @Inject
   Mailer mailer;

   public void sendEmail(final User user) {
       mailer.send(
           Mail.withText(user.email, "Your New Password",
                           String.format("New Password %s.", user.password))
       );
   }
}

Now, we need to write a test for this class. One of the approaches that might come to mind is to use Mockito as we’ve seen in the previous article, and it’s a fair point, but Quarkus offers a stubbed mail client which is injected automatically in your code basis in dev and test profiles. Since it’s stubbed, you can query it for getting the total number of messages sent, getting all messages sent by a user, or wiping them out.

Since this stubbed mail client is automatically used in the test profile, we can inject an instance of io.quarkus.mailer.MockMailbox in our test and use it in the assertions section. In the following snippet you see what a MailService test looks like:

@QuarkusTest
public class MailServiceTest {
  
   @Inject
   MockMailbox mailbox;

   @Inject
   MailService mailService;

   @BeforeEach
   void init() {
       mailbox.clear();
   }

   @Test
   public void shouldSendAnEmail() {
       User user = new User("Alex", "alex@example.com", "abcd");

       mailService.sendEmail(user);

       assertThat(mailbox.getMessagesSentTo("alex@example.com"))
           .hasSize(1)
           .extracting("subject")
           .containsExactly("Your New Password");

   }
}
  • MockMailbox is the class where all emails are sent. No real mail server is used.
  • MailService is injected as it’s the business logic under test.
  • Before every test run, the mailbox is cleaned so tests are isolated.
  • mailbox.getMessagesSentTo("alex@example.com") returns a list of all the emails sent by the alex@example.com account.

INFO: All messages are printed to the Quarkus terminal when MockMailbox is enabled.

TIP: You can disable MockMailbox injection in dev and test profiles by setting the quarkus.mailer.mock configuration property to false.

Security and RBAC

So far, we’ve been accessing the REST endpoints freely without any kind of auth mechanism. While this might work for some endpoints, some others (especially the ones related to administration tasks) need to be protected.

Quarkus Security integrates with the JavaEE security model (i.e., javax.annotation.security.RolesAllowed annotation) and provides multiple authentication and authorization mechanisms to use. To cite a few: OpenId Connect, JWT, Basic Auth, OAuth 2, JDBC, or LDAP.

Let’s protect the findUserByUsername endpoint to limit the access to the Admin role as this is something that a regular user should never do:

@GET
@RolesAllowed("Admin")
@Path("/{username}")
@Produces(MediaType.APPLICATION_JSON)
public Response findUserByUsername(@PathParam("username") String username) {
}

Write a white-box test

Now, it’s time to write a white-box test using RestAssured to validate we can find a user by its username. (We’ve already done this in part 1 of this article.) The test for this endpoint is shown in the following snippet:

@Test
@Order(2)
public void shouldFindAUserByUsername() {
   given()
      .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
      .when().get("/{username}", "Alex")
      .then()
         .statusCode(200)
         .body("username", is("Alex"))
         .body("email", is("asotobu@example.com"))
         .body("password", is("my-secret-password"));
  • There is a test method executed before this test that inserts a user.
  • RestAssured is used to call the endpoint and also validates the response.

But, what happens when we execute this logic? Exactly! It fails because the status code isn’t 200 OK but 401 Unauthorized as the endpoint is protected and we’ve not been authenticated in the system.

So at this point, we have two options—the first one is to authenticate in the system. This could be a good approach but:

  1. The logic to authenticate against the system might not be easy. Think, for example, in the case of OAuth2. To run the test we would need to have an Identity provider like Keycloak configured with some test data, and the test needs to execute some logic to authenticate against the identity provider following the security protocol.
  2. The test should run fast—it now runs much slower as there is an overhead in the preparation of the test.
  3. Any change to the authentication mechanism affects all tests and adds a requirement/prerequisite.

Obviously, we need to test if the real security mechanism works, but this can be implemented in specific security tests, and not in tests where we’re validating the business logic. For this reason, let’s explore a second option.

Test Security

Quarkus has a Quarkus Test Security module that lets you modify the security context during the test phase.

To use the Quarkus Test Security module, we need to add the quarkus-test-security dependency in our build tool script. For example, in Maven you should add the following section in pom.xml:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-test-security</artifactId>
  <scope>test</scope>
</dependency>

This module provides the io.quarkus.test.security.TestSecurity annotation to control the security context the test is run with. Basically, you can either bypass authorization so tests can access secured endpoints without needing to be authenticated, and/or you can specify the user and roles you want the tests to use.

Let’s rewrite the previous test with authorization disabled.

@Test
@Order(2)
@TestSecurity(authorizationEnabled = false)
public void shouldFindAUserByUsername() {
  ...
}
  • The test can access secured endpoints without needing to authenticate.

If we now rerun the test, the test succeeds as security is disabled for this specific test.

We can also use the same annotation to configure the current user/roles the test will run as:

@Test
@Order(2)
@TestSecurity(user = "john", roles = "Admin")
public void shouldFindAUserByUsername() {
   ...
}

If we run the test again, it passes because the user and the role match the security constraints. Change the roles attribute from Admin to Admin2, and the test will fail because of a security issue.

Callbacks

Quarkus has an extension mechanism to enrich all your @QuarkusTest classes by implementing the following callback interfaces:

  • io.quarkus.test.junit.callback.QuarkusTestBeforeClassCallback executes the logic before any test class execution.
  • io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback executes the logic before each test method execution.
  • io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback executes the logic before each test method execution.

Let’s create a callback that is executed before any test method, printing the current test method and all annotations placed at the class level.

public class MyQuarkusTestBeforeEachCallback implements QuarkusTestBeforeEachCallback {

   @Override
   public void beforeEach(QuarkusTestMethodContext context) {
       System.out.println("Executing " + context.getTestMethod());

       Annotation[] annotations = context.getTestInstance().getClass().getAnnotations();
       Arrays.stream(annotations)
           .forEach(System.out::println);

   }
}
  • We can get the current test method using the context object.
  • The other element we can get is the current test instance. With the instance object, we can inject attributes to the test instance, read values, or read static meta-information like annotations.

Quarkus test callbacks need to be registered as a Java service provider. Create the following file:

src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback 

with the following content:

org.acme.MyQuarkusTestBeforeEachCallback

Then for every test run, org.acme.MyQuarkusTestBeforeEachCallback is executed.

IMPORTANT:             While it’s possible to use JUnit Jupiter callback interfaces, you might run into classloading issues because Quarkus has to run tests in a custom classloader which JUnit isn’t aware of.

Callbacks are a nice way to execute some logic before/after each test in a reusable way. A good use case for using callbacks could be the encapsulation of the authentication logic for your end-to-end tests. Instead of repeating the authentication logic in each test class, we could just delegate it to a callback.

import org.acme.api.OpenIdAuthentication;

public class OpenIdAuthenticationTestBeforeClassCallback implements QuarkusTestBeforeClassCallback {

   @Override
   public void beforeClass(Class<?> testClass) {
      
       OpenIdAuthentication openIdAuth = testClass.getAnnotation(OpenIdAuthentication.class);

       if (openIdAuth != null) {

           String accessToken = getToken(openIdAuth);

           final RequestSpecBuilder requestSpec = new RequestSpecBuilder();
           requestSpec.addHeader("Authorization", "Bearer " + accessToken);
          
           RestAssured.requestSpecification = requestSpec.build();
       }
      
   }  
}

And then you only need to annotate tests with OpenIdAuthentication.

@OpenIdAuthentication(username = "Ada", password = "Alexandra")
@QuarkusTest
public class RegeneratePasswordTest {
}

TIP: Callbacks aren’t CDI-aware so you cannot inject any CDI bean in the class. If you need to do it you can always rely on programmatic lookup via the io.quarkus.arc.Arc.container() method.

With callbacks, we can implement reusable test logic that is executed before or after tests, but callbacks don’t cover all possible use cases we might find when developing tests. One of these common needs is to execute some logic before the Quarkus application starts, reconfigure Quarkus with specific configuration properties, and execute some logic before the Quarkus application stops. For example, starting a Docker Container using Testcontainers (https://www.testcontainers.org/) project, use the container during test execution, and at the end stop it.

So let’s see how to achieve this in Quarkus tests.

Quarkus Test Resource

Quarkus has a mechanism to execute some logic before the application is up and when it’s stopped as well as reconfigure the application with new values during test execution.

We only need to create a class implementing io.quarkus.test.common.QuarkusTestResourceLifecycleManager and annotating one test of the test suite with @QuarkusTestResource.

If multiple Quarkus Test Resources are defined, @QuarkusTestResource has the parallel attribute to start them concurrently.

Testcontainers

Testcontainers is a Java library that provides a way to start/stop Docker containers programmatically from the Java code, it supports out-of-the-box most used database containers, Kafka, Localstack, or WebDrivers to name a few.

To use the Testcontainers, we need to add the testcontainers dependencies in our build tool script. For example in Maven you should add the following section in pom.xml:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>1.15.1</version>
</dependency>
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>mariadb</artifactId>
  <version>1.15.1</version>
</dependency>

Persistence Integration Tests

Let’s write a persistence integration test but instead of using an embedded in-memory database as we did in part 1, we use a Dockerized MariaDB database.

The first thing to do is to create a class implementing the QuarkusTestResourceLifecycleManager interface. This class starts/stops a Dockerized MariaDB instance and configures the Quarkus data source to use it.


import org.testcontainers.containers.MariaDBContainer;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

public class MariaDBResource implements QuarkusTestResourceLifecycleManager {

   static MariaDBContainer<?> db = new MariaDBContainer<>("mariadb:10.3.6")
                                           .withDatabaseName("mydb")
                                           .withUsername("developer")
                                           .withPassword("developer");

   @Override
   public Map<String, String> start() {
       db.start();
      
       final Map<String, String> conf = new HashMap<>();
       conf.put("%test.quarkus.datasource.jdbc.url", db.getJdbcUrl());
       conf.put("%test.quarkus.datasource.username", "developer");
       conf.put("%test.quarkus.datasource.password", "developer");
       conf.put("%test.quarkus.datasource.db-kind", "mariadb");
      
       return conf;

   }

   @Override
   public void stop() {
      db.stop();
   }  
}
  • MariaDBContainer class encapsulates all the logic to deal with the lifecycle of MariaDB containers.
  • A Map is returned with the new data source configuration used by the Quarkus application.

The second step is to annotate one of our tests with io.quarkus.test.common.QuarkusTestResource annotation. It’s important to notice that although only one class is annotated, the logic is executed only once before the application is started hence the logic is only executed once for the whole test suite.

@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestHTTPEndpoint(RegistrationResource.class)
@QuarkusTestResource(MariaDBResource.class)
public class RegistrationResourceTest {

   @TestHTTPResource
   @TestHTTPEndpoint(RegistrationResource.class)
   URL url;

   @InjectMock
   PasswordGenerator passwordGenerator;

   @Test
   @Order(1)
   public void shouldRegisterAUser() {
      
       Mockito.when(passwordGenerator.generate()).thenReturn("my-secret-password");

       final User user = new User();
       user.username = "Alex";
       user.email = "asotobu@example.com";

       given()
         .body(user)
         .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
         .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
         .when().post()
         .then()
            .statusCode(Status.CREATED.getStatusCode())
            .header("location", url + "/1");
   }
}

Implementation of QuarkusTestResourceLifecycleManager is set on QuarkusTestResource annotation.

Jan 18, 2021 5:06:25 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MariaDB103Dialect
2021-01-18 17:06:14,283 INFO  [org.tes.doc.DockerClientProviderStrategy] (main) Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
2021-01-18 17:06:15,043 INFO  [org.tes.doc.DockerClientProviderStrategy] (main) Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
2021-01-18 17:06:15,044 INFO  [org.tes.DockerClientFactory] (main) Docker host IP address is localhost
2021-01-18 17:06:15,085 INFO  [org.tes.DockerClientFactory] (main) Connected to docker:
  Server Version: 19.03.8
  API Version: 1.40
  Operating System: Docker Desktop
  Total Memory: 7964 MB
2021-01-18 17:06:15,088 INFO  [org.tes.uti.ImageNameSubstitutor] (main) Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
2021-01-18 17:06:15,841 INFO  [org.tes.DockerClientFactory] (main) Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
2021-01-18 17:06:15,841 INFO  [org.tes.DockerClientFactory] (main) Checking the system...
2021-01-18 17:06:15,842 INFO  [org.tes.DockerClientFactory] (main) ✔︎ Docker server version should be at least 1.6.0
2021-01-18 17:06:15,955 INFO  [org.tes.DockerClientFactory] (main) ✔︎ Docker environment should have more than 2GB free disk space
2021-01-18 17:06:16,125 INFO  [ .3.6]] (main) Creating container for image: mariadb:10.3.6
2021-01-18 17:06:16,282 INFO  [ .3.6]] (main) Starting container with ID: e7206ee3b7f529526e0207321df3ea67487d8ab0e652bba9d6bc0200bc9bd61a
2021-01-18 17:06:16,543 INFO  [ .3.6]] (main) Container mariadb:10.3.6 is starting: e7206ee3b7f529526e0207321df3ea67487d8ab0e652bba9d6bc0200bc9bd61a
2021-01-18 17:06:16,555 INFO  [ .3.6]] (main) Waiting for database connection to become available at jdbc:mariadb://localhost:32791/mydb using query 'SELECT 1'
2021-01-18 17:06:24,613 INFO  [ .3.6]] (main) Container is started (JDBC URL: jdbc:mariadb://localhost:32791/mydb)
2021-01-18 17:06:24,613 INFO  [ .3.6]] (main) Container mariadb:10.3.6 started in PT8.651928S

The important part of these lines is that a MariaDB Docker container is started automatically. Then tests are executed using this instance as a database and finally, the instance is stopped.

Out-of-the-box Quarkus Test Resources

Some of the Quarkus extensions provide already implemented Quarkus Test Resources we can use in our tests. In the following table, the most important ones are shown:

Purpose

Dependency

Quarkus Test Resource

Description

SQL

io.quarkus:quarkus-test-h2

H2DatabaseTestResource

Starts a local H2 instance in server mode

LDAP

io.quarkus:quarkus-test-ldap

LdapServerTestResource

Starts a local InMemory LDAP. with dc=quarkus,dc=io and binding credentials ("uid=admin,ou=system", "secret"). Imports LDIF from a file located at the root of the classpath named quarkus-io.ldif

Kubernetes

io.quarkus:quarkus-test-kubernetes-client

KubernetesMockServerTestResource

Starts a local stub of Kubernetes API server and sets the proper environment variables needed by Kubernetes Client.

You can record the expectations by injecting the following instance in your test:

@MockServer private KubernetesMockServer mockServer;

Vault

io.quarkus:quarkus-test-vault

VaultTestLifecycleManager

Starts a Hashicorp Vault instance

JMS

io.quarkus:quarkus-test-artemis

ArtemisTestResource

Starts a local Embedded Artemis instance

SQL

io.quarkus:quarkus-test-derby

DerbyDatabaseTestResource

Starts a local Derby instance

We’ve covered only persistence tests, but we can use Quarkus Test Resouces for any dependencies our tests might need like a Kafka cluster, a Mail server, or a service developed by another team.

At this point, we know how to write integration persistence tests using the same database server used on production, but we still have a typical use case in services architecture that hasn’t been covered yet in the article. How do we test the communication between services? Let’s see how to do it in the following section.

REST Client

Let’s add a new restriction to our registration service. Suppose there is a service that checks if a user name is banned from being used (offensive nicknames, invalid chars, …) and we need to call it before a new user is inserted into our system to avoid any violation of the rules. In the following figure, you can see what the system looks like:

Banned User Service has a simple endpoint that returns true if a username is invalid or false otherwise. The endpoint is a GET /api/<username>.

Quarkus integrates with the MicroProfile Rest Client specification through the quarkus-rest-client extension to provide a type-safe approach to invoke RESTful services over HTTP. The scope of this article isn’t to explain how to develop a Rest Client in Quarkus, hence only the important bits for testing purposes are shown. Let’s implement the logic:

The first thing to do is to create an interface mapping the REST interactions.

@Path("/api")
@RegisterRestClient
@ApplicationScoped
public interface BannedUserClient {
   @GET @Path("/{username}")
   @Produces(MediaType.TEXT_PLAIN)
   String isBanned(@PathParam("username") String username);
}

Then a CDI bean is created to wrap the interaction with the external service, although in this case, it’s pretty straightforward, in other cases, it might need more complicated logic.

@ApplicationScoped
public class BannedUserService {
  
   @RestClient
   BannedUserClient bannedUserClient;

   public boolean isBanned(String username) {
       String banned = bannedUserClient.isBanned(username);
       return Boolean.valueOf(banned);
   }
}

Registration endpoint is updated with these changes:

@Path("/registration")
public class RegistrationResource {

   @Inject
   BannedUserService bannedUserService;
   
   @POST
   @Transactional
   @Consumes(MediaType.APPLICATION_JSON)
   public Response insertUser(User user) {   
      
      if (bannedUserService.isBanned(user.username)) {
         return Response.status(Status.PRECONDITION_FAILED.getStatusCode())
.build();
      } else {
         …
      }
}

Finally, the Banned User Service hostname is configured in the application.properties file.

org.acme.BannedUserClient/mp-rest/url=http://banned-user-service

At this point, we need to update the RegistrationResourceTest with the introduced changes. And we have 4 possible options:

  • Change nothing and run the test against an already running instance of Banned User Service. That’s the easiest way, yet the one you can end up with flaky tests if the service has a downtime.
  • Start a local instance of Banned User Service. That’s a fair strategy and might work in simple services, but if the service has dependencies on other services or databases, then it might be hard to run and maintain these tests.
  • Mock the Rest Client. This is the most used strategy as it’s easy to use and has no external dependency.
  • Using Service Virtualization tooling to not mock at the object level but at the network level.

The latter two options are the most used when testing services architecture, so let’s see how to implement them in Quarkus tests:

Mocks

We’ve seen mocks in part 1 of this Quarkus Testing series, but mocking the Rest client interface requires a small change.

Remember that to use Mockito we need to register io.quarkus:quarkus-junit5-mockito dependency into the build tool.

The biggest difference when mocking the Rest client interface in contrast to mocking a simple CDI bean is that org.eclipse.microprofile.rest.client.inject.RestClient annotation is required together with io.quarkus.test.junit.mockito.InjectMock.

Let’s write a test that verifies when a username is invalid, a 421 Precondition Failed status code is returned.

@QuarkusTest
@TestHTTPEndpoint(RegistrationResource.class)
@QuarkusTestResource(MariaDBResource.class)
public class RegistrationResourceTest {
   @InjectMock
   @RestClient
   BannedUserClient bannedUserClient;

   @Test
   public void shouldNotAddAUserIfBannedName() {
     Mockito.when(bannedUserClient.isBanned("Alex")).thenReturn("true");

     final User user = new User();
     user.username = "Alex";
     user.email = "asotobu@example.com";

     given()
       .body(user)
       .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
       .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
       .when().post()
       .then()
         .statusCode(Status.PRECONDITION_FAILED.getStatusCode());
   }
} 
  • org.eclipse.microprofile.rest.client.inject.RestClient and io.quarkus.test.junit.mockito.InjectMock are used together to inject a mock of the Rest Client interface.
  • org.mockito.Mockito class is used as we normally do.

The main benefit of this approach is that it’s really easy to implement without any performance penalties. This works in most of our tests, but when we’re writing integration tests for the Rest client, we don’t want to mock the interface because what we really want is to test the full call stack, from code to network and back. Using the real service might not be an option for the reasons we saw before, so this is when service virtualization enters into the scene:

Service Virtualization

Service virtualization is a technique used to simulate the behavior of dependencies of a service. Although service virtualization is commonly associated with REST API-based services, the same concept can be applied to any other kind of dependencies like databases, ESBs, JMS, …

Service virtualization creates a server proxy where any request is intercepted and a canned response is provided to the caller. From the point of view of the service under test, the request is sent to a real server so all stack is tested.

Let’s see how we can use service virtualization with Quarkus.

Hoverfly

Hoverfly  is an open-source, lightweight, service virtualization API simulation tool written in the Go programming language. It also offers language bindings that tightly integrate with Java (https://docs.hoverfly.io/projects/hoverfly-java/en/latest/).

To use the Hoverfly, we need to add the Hoverfly dependencies in our build tool script. For example in Maven you should add the following section in pom.xml:

<dependency>
  <groupId>io.specto</groupId>
  <artifactId>hoverfly-java</artifactId>
  <version>0.14.0</version>
  <scope>test</scope>
</dependency>

Then let’s develop a Quarkus Test Resource that starts/stops the Hoverfly server proxy and records some canned answers.


import static io.specto.hoverfly.junit.core.HoverflyConfig.localConfigs;
import static io.specto.hoverfly.junit.core.SimulationSource.dsl;
import static io.specto.hoverfly.junit.dsl.HoverflyDsl.service;
import static io.specto.hoverfly.junit.dsl.ResponseCreators.success;
import static io.specto.hoverfly.junit.core.HoverflyMode.SIMULATE;
import io.specto.hoverfly.junit.core.Hoverfly;

public class HoverflyResource implements QuarkusTestResourceLifecycleManager {

   private Hoverfly hoverfly;

   @Override
   public Map<String, String> start() {
       hoverfly = new Hoverfly(localConfigs().destination("banned-user-service"), SIMULATE);

       hoverfly.start();
       hoverfly.simulate(
           dsl(
               service("banned-user-service")
               .get("/api/Alex")
               .willReturn(success("true", MediaType.TEXT_HTML))
               .get("/api/Ada")
               .willReturn(success("false", MediaType.TEXT_HTML))
           )
       );
      
       return null;
   }

   @Override
   public void stop() {
       hoverfly.close();
   }
}
  • Hoverfly class is used to control the lifecycle of server proxy.
  • Service method sets the hostname under simulation. That’s the host configured in application.properties.
  • Two interactions are recorded, one returning the username is banned and the other not.

Now, we can remove the mocking reference of the RegistrationResourceTest test and annotate it with HoverflyResource.

@QuarkusTest
@TestHTTPEndpoint(RegistrationResource.class)
@QuarkusTestResource(MariaDBResource.class)
@QuarkusTestResource(HoverflyResource.class)
public class RegistrationResourceTest {

   @InjectMock
   PasswordGenerator passwordGenerator;

   @Test
   public void shouldNotAddAUserIfBannedName() {

     final User user = new User();
     user.username = "Alex";
     user.email = "a@example.com";

     given()
       .body(user)
       .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
       .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
       .when().post()
       .then()
         .statusCode(Status.PRECONDITION_FAILED.getStatusCode());
   }
}

There is no mocking code, the test just assumes a remote service is up and running. In fact, there is one—not the real Banned Users Service but a proxied one. Inspecting the console, you’ll see Hoverfly running:

2021-01-19 14:55:52,716 INFO  [hoverfly] (Thread-43) Default proxy port has been overwritten port=55949
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) Default admin port has been overwritten port=55950
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) Using memory backend 
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) Proxy prepared... Destination=. Mode=simulate ProxyPort=55949
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) current proxy configuration destination=. mode=simulate port=55949
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) serving proxy 
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) Admin interface is starting... AdminPort=55950
2021-01-19 14:55:52,776 INFO  [io.spe.hov.jun.cor.Hoverfly] (main) A local Hoverfly with version v1.3.1 is ready
2021-01-19 14:55:52,782 INFO  [hoverfly] (Thread-43) Mode has been changed mode=simulate

Hoverfly is started and canned answers are recorded. Notice this test has two Quarkus Test Resources registered, one for MariaDB and another one for Hoverfly.

Conclusions

We’ve been digging into Quarkus testing, how to bypass security constraints for testing purposes, how to use Testcontainers for writing integration tests, and finally how to test when a service has a dependency on another service.

But there are still a few testing tips and tricks in Quarkus not yet covered. For example, how to test reactive/synchronous code when using Kafka or when periodic tasks are set. We will cover these topics in part 3 of this article.

Source Code

About the Author

Alex Soto is a Director of Developer Experience at Red Hat. He is passionate about the Java world, software automation and he believes in the open-source software model. Alex is the co-author of Testing Java Microservices and Quarkus cookbook books and contributor to several open-source projects. A Java Champion since 2017, he is also an international speaker and teacher at Salle URL University. You can follow him on Twitter (@alexsotob) to stay tuned to what’s going on in Kubernetes and Java world.

Rate this Article

Adoption
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT

Is your profile up-to-date? Please take a moment to review and update.

Note: If updating/changing your email, a validation request will be sent

Company name:
Company role:
Company size:
Country/Zone:
State/Province/Region:
You will be sent an email to validate the new email address. This pop-up will close itself in a few moments.