Key Takeaways
- Quarkus is a full-stack, Kubernetes-native Java framework made for Java virtual machines (JVMs) and native compilation.
- 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.
- Here we start with a full verification of a sample application's logic, and then step by step see how we can reduce the scope of the test by using some of the Quarkus testing facilities.
- Although the native compilation process should work all the time, it’s also true that when a native executable is run, a few issues can occur. For this reason, it’s a good approach to run some tests against the native executable binary.
- Writing tests in Quarkus isn’t particularly challenging. The framework provides enough tooling to create stubs and easily create mocks (via Mockito, PanacheMock), as well as make your tests nondependent on the environment by using the @TestHTTPEndpoint annotation.
What is Quarkus?Quarkus is a full-stack, Kubernetes-native Java framework made for Java virtual machines (JVMs) and native compilation.
Quarkus optimizes Java specifically for containers and enables 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 component/integration tests for Quarkus applications.
We aren’t going to see unit tests for one simple reason: unit tests are written without having the runtime environment in the context of the test, so usually, they are just written with a few libraries—in my opinion, Junit5 + Mockito + AssertJ is the best combination. So everything you have been using for your existing unit tests is valid in a Quarkus application.
But how about component/integration tests where the runtime environment comes into the scene? Let’s learn how to write them in Quarkus.
The application
Let’s consider a small use case where we are building a REST API to register users.
Any user that wants to register to our system, needs to provide us a username and an email. Then an automatic password is generated and the user is stored in the database.
If we speak about the design of the application, it’s composed of the following classes:
We aren’t going to deeply inspect the code of the application, just the necessary parts required for the tests.
RegistrationResource
First of all, you can see the REST API endpoint that implements the registration operations:
@Path("/registration")
public class RegistrationResource {
@Inject
PasswordGenerator passwordGenerator;
@GET
@Path("/{username}")
@Produces(MediaType.APPLICATION_JSON)
public Response findUserByUsername(@PathParam("username") String username) {
return User.findUserByUsername(username)
.map(u -> Response.ok(u).build())
.orElseGet(() -> Response.status(Status.NOT_FOUND).build());
}
@POST
@Transactional
@Consumes(MediaType.APPLICATION_JSON)
public Response insertUser(User user) {
user.password = passwordGenerator.generate();
user.persist();
URI userUri = UriBuilder.fromResource(RegistrationResource.class)
.path("/" + user.id).build();
return Response
.created(userUri)
.build();
}
}
Important things in this class:
- There are 2 endpoints, one to insert a user and one to get information about it.
- It delegates the password generation to an implementation of the
PasswordGenerator
class. It’s injected using CDI (Context and Dependency Injection). - User is an implementation of the Active Record Pattern. More about this later.
User
The user entity uses the JPA (Java Persistence API) spec and implements the Active Record Pattern. For this reason, it extends the PanacheEntity
class.
@Entity
public class User extends PanacheEntity {
@Column public String username;
@Column public String email;
@Column public String password;
public static Optional<User> findUserByUsername(String username) {
return find("username", username).firstResultOptional();
}
}
PasswordGenerator
The last important class is the password generator that creates a random password for the user.
@ApplicationScoped
public class RandomPasswordGenerator implements PasswordGenerator {
@Override
public String generate() {
return UUID.randomUUID().toString();
}
}
It’s important to note here that the RandomPasswordGenerator
is a bean managed by the CDI container because of ApplicationScoped
annotation.
Testing
We’re going to start with a full logic verification, and then step by step see how we can reduce the scope of the test by using some of the Quarkus testing facilities.
Let’s start writing a component test that verifies all the logic is executed correctly. I know some of you might be wondering “isn’t this an end-to-end test?” And it’s a fair question, but wait for a second and you’ll understand why I call it a component test.
Component Testing. Testing all logic.
We want to write tests for the REST API class to check whether each REST endpoint is giving the proper HTTP ResponseCode or not, if it returns the expected JSON or not, etc.
The first thing we need to fix is the database server. If we want to validate that the whole flow works as expected, we need a database so users can be stored and find them later on. We could use the same database as in production, but this would slow down the test execution as the database would be deployed externally of the test, which means that the tests should wait until the database is up and running and then execute all database operations to a remote instance.
Since we want a fast test execution and we don’t want to write an end-to-end test or an integration test, we use an embedded in-memory database like H2.
Can we setup a concrete data source configuration just for the test execution? The answer is yes, with Quarkus profiles.
Quarkus allows you to have multiple configuration values for the same property by prefixing it with a profile.
We can use three profiles by default:
dev
: Activated when in development mode (quarkus:dev
)test
: Activated when running testsprod
: The default profile when not running in development or test mode
To set configuration values for each profile we need to open the configuration file located at src/main/resources/application.properties
and use the following syntax: .config.key=value
.
An example of configuring two JDBC connections, one for testing and another one for production is shown in the next snippet:
quarkus.datasource.jdbc.url=jdbc:mariadb://localhost:3306/mydb
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1
Let’s open the application.properties file and configure a data source to be used during test execution:
%test.quarkus.datasource.db-kind=h2
%test.quarkus.datasource.username=sa
%test.quarkus.datasource.password=
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1
%test.quarkus.hibernate-orm.database.generation=update
In this case, H2 is used as an embedded in-memory database during test execution.
Quarkus relies on JUnit 5 as a testing framework and REST-Assured for testing and validating REST services.
The most important annotation in a Quarkus test is the @QuarkusTest annotation. This annotation is used to start the Quarkus application on port 8081. Even though you’ve got several test classes (all of them annotated with @QuarkusTest), the process of starting the application is executed only once by default for the whole test suite.
Let’s write a test class for our white-box component test. At this time, we are going to verify that a user is registered.
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response.Status;
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class RegistrationResourceTest {
@Test
@Order(1)
public void shouldRegisterAUser() {
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("/registration")
.then()
.statusCode(Status.CREATED.getStatusCode())
.header("location", "http://localhost:8081/registration/1");
}
}
- The test is annotated with
@QuarkusTest
. @TestMethodOrder
is a JUnit 5 annotation to set the execution order of each test case.@Order
specifies the order when the test case is run.- REST-Assured starts with the
given()
static method. - The
body()
method is used to set the request body message. - The base URL is set automatically (
http://localhost:8081
) and we only need to set the HTTP method and the path using the post("/registration")method. - It verifies that the status code is
201
and the location header value is the correct one.
We can do something similar to test that a user can be retrieved from the application:
@Test
@Order(2)
public void shouldFindAUserByUsername() {
given()
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
.when()
.get("/registration/{username}", "Alex")
.then()
.statusCode(200)
.body("username", is("Alex"))
.body("email", is("asotobu@example.com"))
.body("password", notNullValue());
- The HTTP method now is a get so we use the
get()
method. - After the then() method comes the assertions. The
body()
method is used to validate the response of the body content. The first parameter is a JSON path expression to specify the JSON field we want to verify and the second parameter is the expected value.
In both tests, you might notice that although the base URL is automatically set in REST-Assured, the path isn’t and we need to do it manually (i.e., .post("/registration")
). Quarkus uses @io.quarkus.test.common.http.TestHTTPEndpoint
to inject path configuration into REST-Assured. So let’s rewrite the test so we don’t need to set in every request the subpath.
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestHTTPEndpoint(RegistrationResource.class)
public class RegistrationResourceTest {
@Test
@Order(1)
public void shouldRegisterAUser() {
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", "http://localhost:8081/registration/1");
- In the
TestHTTPEndpoint
annotation, you set the resource under test. - The
post()
method is empty as the path is retrieved automatically from the resource.
Now the test is more resilient to path value changes as it’s automatically configured with REST-Assured.
But still, there is one hard-coded value on the assertion (.header("location", "http://localhost:8081/registration/1")
) so if the test is executed on another port or the path is changed, then the test would fail because of the refactor. Quarkus uses the @io.quarkus.test.common.http.TestHTTPResource
annotation to inject the endpoint location into the test.
@TestHTTPResource
@TestHTTPEndpoint(RegistrationResource.class)
URL url;
@Test
@Order(1)
public void shouldRegisterAUser() {
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");
@TestHTTPResource
and@TestHTTPEndpoint(RegistrationResource.class)
are used together to inject the base URL and the path.
Stubbing and Mocking
So far, we’ve tested the whole application, but take a look at one of the assertions of the shouldFindAUserByUsername
, specifically this one: .body("password", notNullValue()
. Notice that we aren’t asserting the value (because it’s generated randomly), we are only verifying that the field is set. But what happens if we want to verify not only the generated password is set, but also that the value is correct? Or what happens if the logic takes one second to generate a password and we want to avoid doing that expensive call in our tests? Then we have two possibilities—stubbing the logic or mocking the logic. Explaining the difference between both approaches is out of the scope of this article, but you can think of a stub like an object that holds predefined data while mocks are objects with canned answers that register calls they receive for future validation.
Let’s see how to use stubs and mocks in a Quarkus test.
Stubbing
To create a stub of RandomPasswordGenerator
, we need to create an implementation of PasswordGenerator
interface at src/test/java
location. Furthermore, this class must be annotated with @io.quarkus.test.Mock
.
@Mock
public class StaticPasswordGenerator implements PasswordGenerator {
@Override
public String generate() {
return "my-secret-password";
}
}
Then we update the test with the concrete password set in the stub class:
@Test
@Order(2)
public void shouldFindAUserByUsername() throws InterruptedException {
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"));
Notice that now, we know the password beforehand, so we can assert a concrete value returned by the stubbed class.
Now when we run the test, instead of injecting the RandomPasswordGenerator
instance, the StaticPasswordGenerator
is injected into the resource class.
The disadvantage of this approach is that the same stub instance is shared across all the test suite execution, which might be valid in some cases but not in others.
Quarkus also supports using the well-known Mockito library to write mocks.
Mocking
To use Mockito in Quarkus, we need to add the quarkus-junit5-mockito
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-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
To create a mock for our test, we need to use the @io.quarkus.test.junit.mockito.InjectMock
annotation to automatically inject the mock into the CDI container.
@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");
@InjectMock
injects a mock into the test. If you are familiar with the Mockito framework, it’s a sort oforg.mockito.Mock
annotation.- The mock only takes effect for the duration of the test method. If you use it in the
@BeforeAll
method then it’s recorded for the whole test class. - The mock created is automatically injected in the CDI container and it’s the instance injected in the
RegistrationResource
instance.
Quarkus offers a pleasant experience when it’s time to write a black-box test, but what about white-box testing, when we care about the internal design of the classes or for example when we don’t want to test the application by doing a REST call but a method call? Quarkus helps you with this too.
Component Testing. White-box Testing.
So far, we’ve written tests that verify the correctness of the application by doing REST API calls, but as we already know, sometimes we don’t want to test the application from outside (black-box) but from inside (white-box). For example, in this category falls some kind of tests such as persistence tests, integration tests, or some component tests.
The good news is that any test in Quarkus is a CDI bean and this means everything that is valid in CDI is also valid in the test, you can use @Inject
to inject any business bean, you can make them @Transactional
, or even create meta-annotations.
Let’s test the RandomPasswordGenerator
class but instead of instantiating it directly in the test, we are going to use the instance that is created by the CDI container, so it’s close to what is used in production.
@QuarkusTest
public class PasswordGeneratorTest {
@Inject
PasswordGenerator passwordGenerator;
@Test
public void shouldGenerateARandomPassword() {
final String password = passwordGenerator.generate();
assertThat(password).containsPattern("[0-9A-F-]+");
}
}
- Since the test is a CDI bean, the
RandomPasswordGenerator
instance is injected. - No REST API occurs, as we are using AssertJ as an assertion library.
Persistence Tests
Persistence tests are another kind of test that are useful to write, especially when no standard queries are written. Since any Quarkus test is a CDI bean, it’s really easy to write persistence tests as any test can be transactional and an entity manager is instantiated.
Let’s write a persistence test that just validates that finding a user by username works.
@QuarkusTest
public class UserTest {
@Test
@Transactional
public void shouldFindUsersByUsername() {
final User user = new User();
user.username = "Alex";
user.email = "asotobu@example.com";
user.persist();
Optional<User> foundUser = User.findUserByUsername("Alex");
assertThat(foundUser)
.isNotEmpty()
.map(u -> u.email)
.contains("asotobu@example.com");
}
}
- The test is transactional to persist the insert.
EntityManager
is injected inside the User entity automatically.
TIP: @Transactional
annotation on tests means that the changes your test makes to the database are persistent, which might have a side-effect in another test. If you want any changes made to be rolled back at the end of the test you can annotate the test with @io.quarkus.test.TestTransaction
.
But what’s happening if we need to test a business logic that contains persistence code, and we don’t want to rely on the database? In a normal case, we could create a mock, but with the Panache Active Record Pattern implementation, the operations are defined in the entity and some of those are static methods (i.e., finders), so how can we mock them?
Mocking Persistence Tests
Let’s suppose, we’ve developed a class to regenerate the password in case the original one is lost.
This class needs to find the User
object and then update the password using the random password generator.
@ApplicationScoped
public class RegeneratePassword {
@Inject
PasswordGenerator passwordGenerator;
@Transactional
public void regenerate(String username) {
final Optional<User> user = User.findUserByUsername(username);
user.map( u -> {
String newPassword = passwordGenerator.generate();
u.password = newPassword;
return u;
});
}
}
To use Mockito and Panache in Quarkus, we need to add the quarkus-panache-mock
dependency on our build tool script. For example, in Maven you should add the following section in pom.xml
:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
To mock RegeneratePassword
dependencies for our test, we need to use the @io.quarkus.test.junit.mockito.InjectMock
annotation for PasswordGenerator
dependency as done in the previous section, and also use the io.quarkus.panache.mock.PanacheMock
class to create a mock of a PanacheEntity
. In our current use case we use a PanacheMock.mock(User.class)
call.
@QuarkusTest
public class RegeneratePasswordTest {
@InjectMock
PasswordGenerator passwordGenerator;
@Inject
RegeneratePassword regeneratePassword;
@Test
public void shouldGenerateANewPassword() {
Mockito.when(passwordGenerator.generate()).thenReturn("my-secret").thenReturn("password");
PanacheMock.mock(User.class);
User user = new User("Alex", "alex@example.com", "my_super_password");
Mockito.when(User.findUserByUsername("Alex")).thenReturn(Optional.of(user));
regeneratePassword.regenerate("Alex");
PanacheMock.verify(User.class, Mockito.times(1)).findUserByUsername("Alex");
assertThat(user.password).isEqualTo("my-secret");
}
}
PanacheMock
is used to mock the entity bean.- Normal Mockito methods are used as normally we do with the static methods.
- To verify any interaction,
PanacheMock.verify
method needs to be used.
Testing Native Executable
One of the nice features provided by Quarkus is the ability to compile the application into a native executable using GraalVM.
Although this process should work all the time, it’s also true that when a native executable is run, a few issues can occur. For this reason, it’s a good approach to run some tests against the native executable file.
Quarkus provides the io.quarkus.test.junit.NativeImageTest
annotation to make tests start the native executable file instead of starting the application in the Java Virtual Machine.
These kinds of tests are usually executed as integration tests
, therefore when a Quarkus project is scaffolded, Maven Failsafe Plugin is used in contrast to Maven Surefire Plugin. This way tests are executed in the integration-test Maven phase instead of the test
phase and tests need to end up with IT instead of Test.
IMPORTANT: You need to have GraalVM installed on your machine in order to compile to a native executable.
Having GraalVM and native-image tools (${GRAALVM_HOME}/bin/gu install native-image)
installed, we need to set the GRAALVM_HOME environment variable to the GraalVM installation directory.
export GRAALVM_HOME=/Users/asotobu/Applications/graalvm-ce-java11-20.2.0/Contents/Home
Let’s write a white-box test that verifies the native file is working correctly.
@NativeImageTest
public class NativeRegistrationResourceIT {
@Test
public void shouldRegisterAUser() {
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());
}
}
NativeImageTest
annotation is used instead ofQuarkusTest
annotation.
Finally, we can create a native executable and run the test by executing the following command:
./mvnw integration-test -Pnative
Conclusions
Writing tests in Quarkus isn’t that hard, and it gives you enough tooling to create stubs and easily create mocks (Mockito, PanacheMock) as well as make your tests nondependent on the environment by using the @TestHTTPEndpoint annotation.
But there are a lot more things Quarkus provides to help you test an application that we will cover in part 2 of this article.
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.