-
Notifications
You must be signed in to change notification settings - Fork 28
1. Getting Started
- JDK 11+
- Maven 3+
- Docker
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
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.
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
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");
}
}
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).
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);
}
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.
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.
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.
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();
//
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());
}
}
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!"));
}
- 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.
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.
For example, let say I'm in Greetings Example directory and I want to debug tests:
- for ReactiveGreetingResourceIT in JVM mode
mvn clean verify -Dit.test=ReactiveGreetingResourceIT -Dts.debug
- for ReactiveGreetingResourceIT in native mode
mvn clean verify -Dit.test=ReactiveGreetingResourceIT -Dts.debug -Dnative
- for OpenShiftReactiveGreetingResourceIT in OpenShift
mvn clean verify -Dit.test=OpenShiftReactiveGreetingResourceIT -Dts.debug -Dopenshift
- for DevModeGreetingResourceIT in DEV mode
mvn clean verify -Dit.test=DevModeGreetingResourceIT -Dts.debug
In case you don't specify any test with -Dit.test
option, the framework will selected random test for you.
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
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.
When you specify -Dts.debug
with -Dit.test
that starts Quarkus application in JVM mode, you can attach your IDE debugger to a process listening on localhost with port 5005
. This won't work for a test class that starts multiple Quarkus applications (because the 5005
port will already be in use). In case you need to support more advanced use cases like that, please open Quarkus QE Test Framework enhancement issue.