Skip to content

1. Getting Started

Michal Vavřík edited this page May 20, 2023 · 14 revisions

Requirements

  • JDK 11+
  • Maven 3+
  • Docker

First Application

First, we need a Quarkus application that we want to verify. If you don't have one, follow the Getting Started from Quarkus guide or simply execute:

mvn io.quarkus.platform:quarkus-maven-plugin:2.3.0.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=getting-started \
    -DclassName="org.acme.getting.started.GreetingResource" \
    -Dpath="/hello"
cd getting-started

The above Maven command will create a Quarkus application with a REST endpoint at /hello.

Then, we need to add the quarkus-test-parent bom dependency under the dependencyManagement section in the pom.xml file:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.quarkus.qe</groupId>
            <artifactId>quarkus-test-parent</artifactId>
            <version>${quarkus.qe.framework.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    <dependencies>
</dependencyManagement>

Be sure you're using the latest version!

Now, we can add the core dependency in the pom.xml file:

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

And finally, let's write our first scenario:

@QuarkusScenario
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("Hello RESTEasy"));
    }

}

Output:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running org.acme.getting.started.GreetingResourceTest
08:38:57,019 INFO  JBoss Threads version 3.4.2.Final
08:38:58,054 Quarkus augmentation completed in 1479ms
08:38:58,054 INFO  Quarkus augmentation completed in 1479ms
08:38:58,072 INFO  [app] Initialize service (Quarkus JVM mode)
08:38:58,085 INFO  Running command: java -Dquarkus.log.console.format=%d{HH:mm:ss,SSS} %s%e%n -Dquarkus.http.port=1101 -jar /home/jcarvaja/sources/tmp/getting-started/target/GreetingResourceTest/app/quarkus-app/quarkus-run.jar
08:39:01,130 INFO  [app] __  ____  __  _____   ___  __ ____  ______ 
08:39:01,134 INFO  [app]  --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
08:39:01,135 INFO  [app]  -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
08:39:01,136 INFO  [app] --\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
08:39:01,137 INFO  [app] 08:38:58,980 Quarkus 2.3.0.Final on JVM started in 0.813s. Listening on: http://0.0.0.0:1101
08:39:01,138 INFO  [app] 08:38:58,985 Profile prod activated. 
08:39:01,139 INFO  [app] 08:38:58,986 Installed features: [cdi, resteasy, smallrye-context-propagation, vertx]
08:39:01,147 INFO  [app] Service started (Quarkus JVM mode)
08:39:01,575 INFO  ## Running test GreetingResourceTest.testHelloEndpoint()
08:39:06,804 INFO  [app] Service stopped (Quarkus JVM mode)
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.72 s - in org.acme.getting.started.GreetingResourceTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Native

Quarkus builds the Native binary after executing the Surefire tests, so we need to configure the Failsafe Maven plugin (for integration tests) to propagate the quarkus.package.type property:

<plugin>
    <artifactId>maven-failsafe-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
            <configuration>
                <systemProperties>
                    <quarkus.package.type>${quarkus.package.type}</quarkus.package.type>
                </systemProperties>
            </configuration>
        </execution>
    </executions>
</plugin>

Now, we can either (1) rename the test from GreetingResourceTest to GreetingResourceIT, so it became an integration test, or (2) create a new integration test called NativeGreetingResourceIT class with parent GreetingResourceTest.

Finally, run the Maven command using the standard Native Quarkus instructions (more in here):

mvn clean verify -Dnative

Output:

[INFO] Running org.acme.getting.started.NativeGreetingResourceIT
09:13:07,239 INFO  [app] Initialize service (Quarkus NATIVE mode)
09:13:07,322 INFO  Running command: /home/jcarvaja/sources/tmp/getting-started/target/getting-started-1.0.0-SNAPSHOT-runner -Dquarkus.log.console.format=%d{HH:mm:ss,SSS} %s%e%n -Dquarkus.http.port=1101
09:13:10,434 INFO  [app] __  ____  __  _____   ___  __ ____  ______ 
09:13:10,439 INFO  [app]  --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
09:13:10,441 INFO  [app]  -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
09:13:10,442 INFO  [app] --\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
09:13:10,443 INFO  [app] 09:13:07,380 getting-started 1.0.0-SNAPSHOT native (powered by Quarkus 2.3.0.Final) started in 0.046s. Listening on: http://0.0.0.0:1101
09:13:10,444 INFO  [app] 09:13:07,380 Profile prod activated. 
09:13:10,445 INFO  [app] 09:13:07,381 Installed features: [cdi, resteasy, smallrye-context-propagation, vertx]
09:13:10,455 INFO  [app] Service started (Quarkus NATIVE mode)
09:13:11,218 INFO  ## Running test NativeGreetingResourceIT.testHelloEndpoint()
09:13:16,596 INFO  [app] Service stopped (Quarkus NATIVE mode)
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.685 s - in org.acme.getting.started.NativeGreetingResourceIT

The test framework will reuse the Native binary generated by Maven to run the test. However, if the scenario provides a build property, then it will generate a new Native binary.

Build/Runtime properties

In Quarkus, the runtime properties are the configuration that users can modify at runtime (after the binary is built) and the build properties are the configuration that can only be set when building the binary (can't change at runtime).

The good news is that using the test framework, you won't need to check whether the property you're adding it's a runtime or a build property! The test framework will autodetect the build/runtime properties and build the artifacts per test scenario if needed.

The service interface provides multiple methods to add properties at test scope only:

  • service.withProperties(path)
  • service.withProperty(key, value)
  • service.withProperty(key, () -> ...): the property value will be evaluated at startup scenario time

For example:

@QuarkusScenario
public class PingPongResourceIT {

    @QuarkusApplication
    static final RestService pingPongApp = new RestService()
        .withProperties("additional.properties")
        .withProperty("io.quarkus.qe.PongClient/mp-rest/url", "http://host:port") // runtime property!
        .withProperty("quarkus.datasource.db-kind", "h2") // build property!
        .withProperty("my.custom.property", () -> "some value"); // future property!

    // ...
}

By default, the test framework will use the application.properties file at src/main/resources folder, if you want to use a different application properties file for all the tests, you can add the application.properties file at src/test/resources and the test framework will use this instead.

Moreover, if you want to select a concrete application properties file for a single test scenario, then you can configure your Quarkus application using:

@QuarkusScenario
public class PingPongResourceIT {

    // Now, the application will use the file `my-custom-properties.properties` instead of the `application.properties` 
    @QuarkusApplication(properties = "my-custom-properties.properties")
    static final RestService pingpong = new RestService();
}

This option is available also for Dev Mode, Remote Dev mode and remote git applications, and works for JVM, Native, OpenShift and Kubernetes.

| Note that the test framework does not support the usage of YAML files yet #240

Logging

All the services provide the logs of the running container or Quarkus application. Example of usage:

@QuarkusScenario
public class DevModeMySqlDatabaseIT {

    @DevModeQuarkusApplication
    static RestService app = new RestService();

    @Test
    public void verifyLogsToAssertDevMode() {
        app.logs().assertContains("Profile dev activated. Live Coding activated");
        // or app.getLogs() to get the full list of logs.
        // or app.logs().forQuarkus().installedFeatures().contains("kubernetes");
    }
}

External Resources

We can use properties that require external resources using the resource:: tag. For example: .withProperty("to.property", "resource::/file.yaml");. This works in bare metal or OpenShift/Kubernetes.

The same works for secret resources: using the secret:: tag. For example: .withProperty("to.property", "secret::/file.yaml");. For baremetal, there is no difference, but when deploying on OCP and Kubernetes, one secret will be pushed instead. This only works for file system resources (secrets from classpath are not supported).

Test Expected Failures

With the test framework, we can assert startup failures using service.setAutoStart(false). When disabling this flag, the test framework will not start the service and users will need to manually start them by doing service.start() at each test case. Hence users should be able now to assert failure messages from the logs for each test case. For example:

@QuarkusApplication
static final RestService app = new RestService()
        .setAutoStart(false);

@Test
public void shouldFailOnStart() {
    assertThrows(AssertionError.class, () -> app.start(),
            "Should fail because runtime exception in ValidateCustomProperty");
    // or checks service logs
    app.logs().assertContains("Missing property a.b.z");
}

Moreover, we can try to fix the application during the test execution:

@Test
public void shouldWorkWhenPropertyIsCorrect() {
    app.withProperty("a.b.z", "here you have!");
    app.start();
    app.given().get("/hello").then().statusCode(HttpStatus.SC_OK);
}

Multiple Quarkus Applications

In the previous versions, we have created our first scenario using the test framework, configured the Failsafe Maven plugin and execute our tests on Native. Let's now create a scenario with multiple Quarkus instances.

First, we're going to create a Ping Pong application with the following endpoints:

PingResource.java:

@Path("/ping")
public class PingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String ping() {
        return "ping";
    }
}

PongResource.java:

@Path("/pong")
public class PongResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String pong() {
        return "pong";
    }
}

Let's write our scenario:

@QuarkusScenario
public class PingPongResourceIT {
    @Test
    public void shouldPingPongWorks() {
        given().get("/ping").then().statusCode(HttpStatus.SC_OK).body(is("ping"));
        given().get("/pong").then().statusCode(HttpStatus.SC_OK).body(is("pong"));
    }
}

In this scenario, we're starting only 1 instance with all the resources, but what about if we want to create multiple instances with different sources. Let's see how we can do it using the test framework:

@QuarkusScenario
public class PingPongResourceIT {

    @QuarkusApplication(classes = PingResource.class)
    static final RestService pingApp = new RestService();

    @QuarkusApplication(classes = PongResource.class)
    static final RestService pongApp = new RestService();

    // will include ping and pong resources
    @QuarkusApplication
    static final RestService pingPongApp = new RestService();

    @Test
    public void shouldPingWorks() {
        ping.given().get("/ping").then().statusCode(HttpStatus.SC_OK).body(is("ping"));
        ping.given().get("/pong").then().statusCode(HttpStatus.SC_NOT_FOUND);
    }

    @Test
    public void shouldPongWorks() {
        pong.given().get("/pong").then().statusCode(HttpStatus.SC_OK).body(is("pong"));
        pong.given().get("/ping").then().statusCode(HttpStatus.SC_NOT_FOUND);
    }

    @Test
    public void shouldPingPongWorks() {
        pingpong.given().get("/ping").then().statusCode(HttpStatus.SC_OK).body(is("ping"));
        pingpong.given().get("/pong").then().statusCode(HttpStatus.SC_OK).body(is("pong"));
    }
}

Simple like that!

We can find more information about this scenario in the examples folder.

Services Start Up Order

In the previous section, we have created a scenario to startup multiple Quarkus applications. By default, the services are initialized in Natural Order of presence. For example, having:

class MyParent {
    @QuarkusApplication // ... or @Container
    static final RestService firstAppInParent = new RestService();
    
    @QuarkusApplication // ... or @Container
    static final RestService secondAppInParent = new RestService();

}

@QuarkusScenario
class MyScenarioIT extends MyParent {
    @QuarkusApplication // ... or @Container
    static final RestService firstAppInChild = new RestService();

    @QuarkusApplication // ... or @Container
    static final RestService secondAppInChild = new RestService();
}

Then, the framework will initialize the services at this order: firstAppInParent, secondAppInParent, firstAppInChild and secondAppInChild.

We can change this order by using the @LookupService annotation:

class MyParent {
    @LookupService
    static final RestService appInChild; // field name must match with the service name declared in MyScenarioIT.

    @QuarkusApplication // ... or @Container
    static final RestService appInParent = new RestService().withProperty("x", () -> appInChild.getHost());
}

@QuarkusScenario
class MyScenarioIT extends MyParent {
    @QuarkusApplication // ... or @Container
    static final RestService appInChild = new RestService();
}

| Note that field name of the @LookupService must match with the service name declared in MyScenarioIT.

Now, the framework will initialize the appInChild service first and then the appInParent service.

Forced Dependencies

We can also specify dependencies per Quarkus application that are not part of the pom.xml by doing:

@QuarkusScenario
public class GreetingResourceIT {

    private static final String HELLO = "Hello";
    private static final String HELLO_PATH = "/hello";

    @QuarkusApplication(dependencies = @Dependency(groupId = "io.quarkus", artifactId = "quarkus-resteasy"))
    static final RestService classic = new RestService();

    @QuarkusApplication(dependencies = @Dependency(groupId = "io.quarkus", artifactId = "quarkus-resteasy-reactive"))
    static final RestService reactive = new RestService();

    @Test
    public void shouldPickTheForcedDependencies() {
        // classic
        classic.given().get(HELLO_PATH).then().body(is(HELLO));
        classic.logs().forQuarkus().installedFeatures().contains("resteasy");

        // reactive
        reactive.given().get(HELLO_PATH).then().body(is(HELLO));
        reactive.logs().forQuarkus().installedFeatures().contains("resteasy-reactive");
    }
}

If no group ID and version provided, the framework will assume that the dependency is a Quarkus extension, so it will use the quarkus.platform.groupId (or io.quarkus) and the default Quarkus version.

This also can be used to append other dependencies apart from Quarkus.

| Note that this feature is not available for Dev Mode and Remote Dev scenarios.

Remote Repositories

We can deploy a remote GIT repository using the annotation @GitRepositoryQuarkusApplication. Example:

@QuarkusScenario
public class QuickstartIT {

    @GitRepositoryQuarkusApplication(repo = "https://github.com/quarkusio/quarkus-quickstarts.git", contextDir = "getting-started")
    static final RestService app = new RestService();
    //

This works on JVM and Native modes. For DEV mode, you need to set the devMode attribute as follows:

@QuarkusScenario
public class DevModeQuickstartIT {

    @GitRepositoryQuarkusApplication(repo = "https://github.com/quarkusio/quarkus-quickstarts.git", contextDir = "getting-started", devMode = true)
    static final RestService app = new RestService();
    //

gRPC Integration

Internally, the test framework will map the gRPC service of our Quarkus application using a random port. This does not work for OpenShift/Kubernetes deployments as it requires to enable HTTP/2 protocol (more information in here).

We can enable the gRPC feature to test Quarkus application using the @QuarkusApplication(grpc = true) annotation. This way we can verify purely gRPC applications using the GrpcService service wrapper:

@QuarkusScenario
public class GrpcServiceIT {

    static final String NAME = "Victor";

    @QuarkusApplication(grpc = true) // enable gRPC support
    static final GrpcService app = new GrpcService();

    @Test
    public void shouldHelloWorldServiceWork() {
        HelloRequest request = HelloRequest.newBuilder().setName(NAME).build();
        HelloReply response = GreeterGrpc.newBlockingStub(app.grpcChannel()).sayHello(request);

        assertEquals("Hello " + NAME, response.getMessage());
    }
}

SSL Integration

This is only supported when running tests on baremetal:

@QuarkusApplication(ssl = true)
static final RestService app = new RestService();

@Test
public void shouldSayHelloWorld() {
    app.https().given().get("/greeting").then().statusCode(HttpStatus.SC_OK).body(is("Hello World!"));
}

Disable Tests annotations

  • On a Concrete Quarkus version:
@QuarkusScenario
@DisabledOnQuarkusVersion(version = "1\\.13\\..*", reason = "https://github.com/quarkusio/quarkus/issues/XXX")
public class GreetingResourceIT {
    
}

This test will not run if the quarkus version is 1.13.X.

Moreover, if we are building Quarkus upstream ourselves, we can also disable tests on Quarkus upstream snapshot version (999-SNAPSHOT) using @DisabledOnQuarkusSnapshot.

  • On Native build:
@DisabledOnNative
public class OnlyOnJvmIT {
    
}

This test will be disabled if we run the test on Native. Similarly, we can enable tests to be run only on Native build by using the @EnabledOnNative annotation.

Debugging

It is also possible to start Quarkus application together with containers and exact configuration for selected test class by adding -Dts.debug to your Maven command. This feature is analogy to adding Thread.sleep(someTime) so some test method.

Why is it useful? You can debug application in any mode and environment (JVM, native, DEV mode, Openshift, Kubernetes) with containers started and configured exactly as they are when running selected test class. When you are done with debugging, simply press enter and environment is going to be swept in a same fashion as it is when tests are run.

OpenShift, JVM, native, DEV mode and more...

For example, let say I'm in Greetings Example directory and I want to debug tests:

In case you don't specify any test with -Dit.test option, the framework will selected random test for you.

Skip @BeforeEach, @BeforeAll, @AfterEach and @AfterAll

By default, methods annotated with @BeforeEach, @BeforeAll, @AfterEach and @AfterAll are invoked before/after debugging. You can disable this with -Dts.debug.skip-before-and-after-methods.

NOTE: non-public methods are always skipped

Run tests before entering debug mode

And what if you want to also run tests before you start debugging? Let say you want to have application and/or containers in state after execution of ReactiveGreetingResourceIT#shouldSayDefaultGreeting, then you will need to add -Dts.debug.run-tests to your Maven command like this mvn clean verify -Dit.test=ReactiveGreetingResourceIT#shouldSayDefaultGreeting -Dts.debug -Dts.debug.run-tests.

NOTE: parametrized tests (methods annotated with @Test that have formal parameters) are excluded from execution and so are non-public test methods

Please bear in mind that pattern matching is simplified here and you can either run all tests in the class or just one of them.