In this tutorial, we’ll have a look at writing tests using Quarkus applications. We’ll cover unit tests that you can run using JUnit Testing Framework and RESTAssured that simplifies the Test of REST Endpoints.
The @QuarkusTest annotation
You can Test Quarkus applications with any Java based testing framework. However, your testing can be greatly simplified by using JUnit 5 testing tool. Also, Quarkus is well integrated with REST-Assured testing framework for validating REST Endpoints.
REST Assured is a Java library that can be used to write powerful tests for REST APIs using a flexible Domain Specific Language (DSL).
The most important part of the Quarkus testing framework is the annotation io.quarkus.test.junit.QuarkusTest.
When you annotate a test class with this annotation, you are selecting that Test to be run according with Quarkus life cycle:
- The Quarkus application starts. When the application is ready to serve request, the Test execution will begin
- Each Test will execute against the running instance
- The Quarkus application stops.
Let’s see a practical example of how to test a REST Endpoint.
Endpoint Resource Testing
Firstly, we will be covering a simple Resource Endpoint test. This is a minimal REST Endpoint you can create using quarkus-resteasy :
@Path("/hero") public class HeroEndpoint { @Inject HeroService service; @GET public List<Hero> list() { return service.getHeros(); } @POST public Response create(Hero hero) { service.add(hero); return Response.ok(hero).status(201).build(); } }
The HeroService Class stores the list of Hero objects in an ArrayList:
@ApplicationScoped public class HeroService { ArrayList<Hero> heros = new ArrayList<Hero>( Arrays.asList(new Hero("Bruce","Wayne"), new Hero("Peter","Parker")) ); public ArrayList<Hero> getHeros() { return heros; } public void add(Hero hero) { getHeros().add(hero); } }
Let’s write our first Test. In order to Test Quarkus applications you can use the io.quarkus.test.junit.QuarkusTest
@QuarkusTest public class HeroEndpointTest { @Test public void testSize() { given() .when().get("/hero") .then() .statusCode(200); } @Test public void testBody() { given() .when().get("/hero") .then() .statusCode(200) .body( containsString("\"name\":\"Bruce\",\"surname\":\"Wayne\""), containsString("\"name\":\"Peter\",\"surname\":\"Parker\"")); } @Test public void testPost() { given() .body("{\"name\": \"Bruce\", \"surname\": \"Banner\"}") .header("Content-Type", "application/json") .when() .post("/hero") .then() .statusCode(201); given() .when().get("/hero") .then() .statusCode(200) .body("$.size()", is(3), "[0].name", is("Bruce"), "[0].surname", is("Wayne"), "[1].name", is("Peter"), "[1].surname", is("Parker"), "[2].name", is("Bruce"), "[2].surname", is("Banner")); } }
- The testSize method shows how you can test the status code of the Rest Endpoint (“/hero”)
- The testBody method shows how to check the GET Response for “/hero” using the containsString method. That will check the actual body content
- The testPost method shows how to send a POST with Restassured and to check the GET Response for “/hero” using another approach. In this case, we are fetching the list of JSON using the array id.
Running the Test
To build a Quarkus application using @QuarkusTest, you need to add the following dependency in your application:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency>
The above dependencies are automatically added when you scaffold a Quarkus application, for example with the Online Initializer. Besides, to configure JSON Serialization, you need to choose one of the available options, for example Jackson or JSON-B:
To run the Test is sufficient to build and install the project:
mvn install
if your IDE supports it, you can also run it from it by running it as JUnit Test:
By default, Quarkus will use port 8081 for running HTTP Tests with a Timeout of 30 seconds for REST Assured tests. If you want to change these defaults, you can use the following configuration properties:
quarkus.http.test-port=8083 quarkus.http.test-timeout=10s
Adding @TestHTTPEndpoint to your Class
Our basic Test example includes the Endpoint Path as part of the test methods:
given() .when().get("/hero") .then() .statusCode(200);
You can decouple the Path of your Endpoint from your code by using the @TestHTTPEndpoint annotation. Example:
@QuarkusTest @TestHTTPEndpoint(HeroEndpoint.class) public class HeroEndpointTest { @Test public void testSize() { given() .when().get() .then() .statusCode(200); } // Other test methods }
As you can see from the above example, your Test methods don’t include any more a reference to the Path. @QuarkusTest automatically retrieves it from the HeroEndpoint @Path
Testing an HTTP Resource of your Quarkus application
The next test example shows how to check the content of a Web resource using the @TestHTTPResource annotation. This annotation is bound to a java.net.URL resource. Therefore you can read the HTTP resource as an InputStream and verify the content of the page. In this example, we are checking that the HTML page contains the title “Quarkus-hibernate example”:
@QuarkusTest public class CustomerEndpointHTTPTest { @TestHTTPResource("index.html") URL url; @Test public void testIndexHtml() throws Exception { try (InputStream in = url.openStream()) { String contents = readStream(in); Assertions.assertTrue(contents.contains("<title>Quarkus-hibernate example</title>")); } } private static String readStream(InputStream in) throws IOException { byte[] data = new byte[1024]; int r; ByteArrayOutputStream out = new ByteArrayOutputStream(); while ((r = in.read(data)) > 0) { out.write(data, 0, r); } return new String(out.toByteArray(), StandardCharsets.UTF_8); } }
Adding Callbacks for each test
JUnit includes out of the box a set of annotations to fire callback methods before/after all tests are executed and before/after each Test execution:
class StandardTests { @BeforeAll static void initAll() { } @BeforeEach void init() { } @AfterEach void tearDown() { } @AfterAll static void tearDownAll() { } }
Besides, if you want to configure Test callbacks specifically for @QuarkusTest you can implement the QuarkusTestBeforeEachCallback and QuarkusTestAfterEachCallback interface. See the following example:
import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback; import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback; import io.quarkus.test.junit.callback.QuarkusTestMethodContext; public class TestCallBackExample implements QuarkusTestBeforeEachCallback, QuarkusTestAfterEachCallback { public void beforeEach(QuarkusTestMethodContext context) { System.out.println("Executing " + context.getTestMethod()); } public void afterEach(QuarkusTestMethodContext context) { System.out.println("Executed " + context.getTestMethod()); } }
In order to register the CallBack classes, you have to add a file under the folder resources/META-INF/services with the name of the interface:
$ tree src/main/resources/ src/main/resources/ ├── application.properties ├── import.sql └── META-INF ├── resources │ └── index.html └── services ├── io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback └── io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback
Within each file, you need to specify the implementing class, which is in our example (for both files):
org.acme.TestCallBackExample
Continuous Testing
To expedite the development process Quarkus supports live coding so that code changes are automatically reflected in your running application. When running in dev mode, you can also combine live coding with continuous testing. This is pretty simple to do, just start your application in dev mode:
$ mvn quarkus:dev
You will see that the Console includes a set of additional options at the bottom:
__ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 2022-05-09 09:19:03,796 INFO [io.quarkus] (Quarkus Main Thread) basic-test 1.0.0-SNAPSHOT on JVM (powered by Quarkus 2.8.3.Final) started in 1.291s. Listening on: http://localhost:8080 2022-05-09 09:19:03,798 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated. 2022-05-09 09:19:03,799 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy-reactive, resteasy-reactive-jackson, smallrye-context-propagation, vertx] -- Tests paused Press [r] to resume testing, [o] Toggle test output, [:] for the terminal, [h] for more options>
If you type “r”, the Tests which are in your project will execute:
All 3 tests are passing (0 skipped), 3 tests were run in 2234ms. Tests completed at 09:23:36.
This allows to apply live changes to your code and re-test it, without stopping Quarkus! If you type “h” from the Console, a list of additional options will display to allow selective testing options:
[r] - Re-run all tests [f] - Re-run failed tests [b] - Toggle 'broken only' mode, where only failing tests are run (disabled) [v] - Print failures from the last test run [p] - Pause tests [o] - Toggle test output (disabled) [i] - Toggle instrumentation based reload (disabled) [l] - Toggle live reload (enabled) [s] - Force restart [h] - Display this help [q] - Quit
Using a specific profile for your Quarkus Tests
Out of the box Quarkus includes the following configuration profiles:
- dev – Triggered when running in development mode (i.e. quarkus:dev)
- test – Triggered when running tests
- prod – This is picked up when not running in development or test mode
You can use the following syntax to bind a configuration parameter to a specific profile:
%{profile}.config.key=value
As an example, you can use the following application.properties configuration file, which defines multiple profiles in it:
%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgresDev %test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:6432/postgresTest %prod.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:7432/postgresProd
In the above configuration, we have specified three different JDBC URLs for our Datasource connection. Each one is bound to a different profile. This way, you can use a configuration specific for the test profile.
Besides that, you can also define custom profiles by implementing the io.quarkus.test.junit.QuarkusTestProfile as in this example:
import io.quarkus.test.junit.QuarkusTestProfile; public class CustomProfile implements QuarkusTestProfile { @Override public Map<String, String> getConfigOverrides() { return Map.of("message", "Hi there!"); } @Override public String getConfigProfile() { return "custom-profile"; } }
To use the custom profile in your tests, just add the @TestProfile with a reference to your Profile class:
import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; @QuarkusTest @TestHTTPEndpoint(CustomerResource.class) @TestProfile(CustomProfile.class) public class CustomProfileTest { @Test public void testCustomProfile() { given() .when().get() .then() .statusCode(200) .body(is("Hi there!")); } }
To learn how to handle threads, timeouts and concurrency in your Tests, continue reading here: Testing with Awaitility made simple
Source code
The source code for this example is available: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/basic-test