blob: 9613399556a2a3c458aa87b363b0a47b4688fdf5 [file] [log] [blame]
= Testing
:page-aliases: testing.adoc
Testing offers a good way to ensure camel routes behave as expected over time.
Before going deeper in the subject, it is strongly advised to read xref:user-guide/first-steps.adoc[First Steps] and https://quarkus.io/guides/getting-started-testing[Quarkus testing].
When it comes to testing a route in the context of Quarkus, the paved road is to write local integration tests.
This has the advantage of covering both JVM and native mode.
The flip side is that the standard Camel testing approach with `camel-test` and `CamelTestSupport` is not supported.
Let's enumerate some points of interest below.
== A test running in JVM mode
The key point is to use the `@QuarkusTest` annotation that will bootstrap Quarkus and start Camel routes before the `@Test` logic is executed,
like in the example beneath:
[source,java]
----
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
@QuarkusTest
class MyTest {
@Test
public void test() {
// Use any suitable code that send test data to the route and then assert outcomes
...
}
}
----
An example implementation can be found https://github.com/apache/camel-quarkus/blob/main/integration-tests/bindy/src/test/java/org/apache/camel/quarkus/component/bindy/it/MessageTest.java[here].
[[native-tests]]
== A test running in native mode
As long as all extensions your application depends on are supported in native mode,
you should definitely test that your application really works in native mode.
The test logic defined in JVM mode can then be reused in native mode thanks to inheriting from the respective JVM mode class.
`@QuarkusIntegrationTest` annotation is there to instruct the Quarkus JUnit extension to compile the application under test to native image
and start it before running the tests.
[source,java]
----
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
class MyIT extends MyTest {
...
}
----
An implementation of a native test may help to capture more details https://github.com/apache/camel-quarkus/blob/main/integration-tests/bindy/src/test/java/org/apache/camel/quarkus/component/bindy/it/MessageRecordIT.java[here].
[[jvm-vs-native-tests]]
== `@QuarkusTest` vs. `@QuarkusIntegrationTest`
JVM mode tests annotated with `@QuarkusTest` are executed in the same JVM as the application under test.
Thanks to that, `@Inject`-ing beans from the application into the test code is possible.
You can also define new beans or even override the beans from the application using `@javax.enterprise.inject.Alternative` and `@javax.annotation.Priority`.
However all these tricks won't work in native mode tests annotated with `@QuarkusIntegrationTest`
because those are executed in a JVM hosted in a process separate from the running native application.
If you ask why, the answer is actually in the previous sentence: a native executable does not need a JVM to run;
it even cannot be run by a JVM, because it is native code, not bytecode.
On the other hand, there is no point in compiling tests to native code. So they are run using a traditional JVM.
An important consequence of this setup is that all communication between tests and the application
must go over network (HTTP/REST, or any other protocol your application speaks)
or through watching filesystem (log files, etc.) or any other kind of interprocess communication.
`QuarkusIntegrationTest` also provides some additional features that are not available through `@QuarkusTest`.
* In JVM mode it can launch and test the runnable application JAR produced by the Quarkus build.
* As mentioned above, in native mode it can launch and test the native application produced by the Quarkus build.
* If a container image was created during the build, then a container is started so that tests can be executed against it.
For more information about `QuarkusIntegrationTest` see the https://quarkus.io/guides/getting-started-testing#quarkus-integration-test[Quarkus testing guide].
== Testing with external services
=== Testcontainers
Sometimes your application needs to access some external service, such as messaging broker, database, etc.
If a container image is available for the service of interest, https://www.testcontainers.org/[Testcontainers]
come in handy for starting and configuring the services during testing.
For the application to work properly it is often essential to pass the connection configuration data
(host, port, user, password, etc. of the remote service) to the application before it starts.
In Quarkus ecosystem, `QuarkusTestResourceLifecycleManager` serves this purpose.
You can start one or more Testcontainers in its `start()` method
and you can return the connection configuration from the method in form of a `Map`.
The entries of this map are then passed to the application either via command line (`-Dkey=value`) in native mode
or through a special MicroProfile configuration provider in JVM mode.
Note that these settings have a higher precedence than the settings in `application.properties` file.
[source,java]
----
import java.util.Map;
import java.util.HashMap;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
public class MyTestResource implements QuarkusTestResourceLifecycleManager {
private GenericContainer myContainer;
@Override
public Map<String, String> start() {
// Start the needed container(s)
myContainer = new GenericContainer(...)
.withExposedPorts(1234)
.waitingFor(Wait.forListeningPort());
myContainer.start();
// Pass the configuration to the application under test
return new HashMap<>() {{
put("my-container.host", container.getContainerIpAddress());
put("my-container.port", "" + container.getMappedPort(1234));
}};
}
@Override
public void stop() {
// Stop the needed container(s)
myContainer.stop();
...
}
----
The defined test resource needs then to be referenced from the test classes with `@QuarkusTestResource` as shown below:
[source,java]
----
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
@QuarkusTestResource(MyTestResource.class)
class MyTest {
...
}
----
Please refer to Camel Quarkus source tree for a https://github.com/apache/camel-quarkus/blob/main/integration-tests/nats/src/test/java/org/apache/camel/quarkus/component/nats/it/NatsTestResource.java[complete example].
=== WireMock
It is sometimes useful to stub HTTP interactions with third party services & APIs so that tests do not have to connect to live endpoints, as this can incur costs and the service may not always be 100% available or reliable.
An excellent tool for mocking & recording HTTP interactions is http://wiremock.org/[WireMock]. It is used extensively throughout the Camel Quarkus test suite for various component extensions. Here follows a typical workflow
for setting up WireMock.
First set up the WireMock server. Note that it is important to configure the Camel component under test to pass any HTTP interactions through the WireMock proxy. This is usually achieved by configuring a component property
that determines the API endpoint URL. Sometimes things are less straightforward and some extra work is required to configure the API client library, as was the case for https://github.com/apache/camel-quarkus/blob/main/integration-tests/twilio/src/main/java/org/apache/camel/quarkus/component/twilio/it/TwilioResource.java#L83[Twilio].
[source,java]
----
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import java.util.HashMap;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
public class WireMockTestResource implements QuarkusTestResourceLifecycleManager {
private WireMockServer server;
@Override
public Map<String, String> start() {
// Setup & start the server
server = new WireMockServer(
wireMockConfig().dynamicPort()
);
server.start();
// Stub a HTTP endpoint. Note that WireMock also supports a record and playback mode
// http://wiremock.org/docs/record-playback/
server.stubFor(
get(urlEqualTo("/api/greeting"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\"message\": \"Hello World\"}")));
// Ensure the camel component API client passes requests through the WireMock proxy
Map<String, String> conf = new HashMap<>();
conf.put("camel.component.foo.server-url", server.baseUrl());
return conf;
}
@Override
public void stop() {
if (server != null) {
server.stop();
}
}
}
----
Finally, ensure your test class has the `@QuarkusTestResource` annotation with the appropriate test resource class specified as the value. The WireMock server will be started before all tests are
executed and will be shut down when all tests are finished.
[source,java]
----
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
@QuarkusTestResource(WireMockTestResource.class)
class MyTest {
...
}
----
More examples of WireMock usage can be found in the Camel Quarkus integration test source tree such as https://github.com/apache/camel-quarkus/tree/main/integration-tests/geocoder[Geocoder].
== `CamelTestSupport` style of testing
If you used plain Camel before, you may know https://camel.apache.org/components/latest/others/test-junit5.html[CamelTestSupport] already.
Unfortunately the Camel variant won't work on Quarkus and so we prepared a replacement called `CamelQuarkusTestSupport`, which can be used in JVM mode.
Please add following dependency into your module (preferably in the `test` scope), to use `CamelQuarkusTestSupport`.
```
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
```
There are several limitations:
* Methods `afterAll`, `afterEach`, `afterTestExecution`, `beforeAll` and `beforeEach` are not executed anymore.
You should use `doAfterAll`, `doAfterConstruct`, `doAfterEach`, `doBeforeEach` instead of them.
Be aware that execution of method `doAfterConstruct` differs from the execution of the method `beforeAll` if `@TestInstance(TestInstance.Lifecycle.PER_METHOD)` is used in which case callback is called
before each test.
* The test class has to be annotated with `@io.quarkus.test.junit.QuarkusTest` and has to extend `org.apache.camel.quarkus.test.CamelQuarkusTestSupport`.
* Camel Quarkus does not support stopping and re-starting the same `CamelContext` instance within the life cycle of a single application. You will be able to call `CamelContext.stop()`, but `CamelContext.start()` won't work.
* Starting and stopping `CamelContext` in Camel Quarkus is generally bound to starting and stopping the application and this holds also when testing.
* Starting and stopping the application under test (and thus also `CamelContext`) is under full control of Quarkus JUnit Extension. It prefers keeping the application up and running unless it is told to do otherwise.
* Hence normally the application under test is started only once for all test classes of the given Maven/Gradle module.
* To force Quarkus JUnit Extension to restart the application (and thus also `CamelContext`) for a given test class, you need to assign a unique `@io.quarkus.test.junit.TestProfile` to that class. Check the https://quarkus.io/guides/getting-started-testing#testing_different_profiles[Quarkus documentation] how you can do that. (Note that `https://quarkus.io/guides/getting-started-testing#quarkus-test-resource[@io.quarkus.test.common.QuarkusTestResource]` has a similar effect.)
* Camel Quarkus executes the production of beans during the build phase. Because all the tests are
build together, exclusion behavior is implemented into `CamelQuarkusTestSupport`. If a producer of the specific type and name is used in one tests, the instance will be the same for the rest of the tests.
* JUnit Jupiter callbacks (`BeforeEachCallback`, `AfterEachCallback`, `AfterAllCallback`, `BeforeAllCallback`, `BeforeTestExecutionCallback` and `AfterTestExecutionCallback`) might not work correctly. See the https://quarkus.io/guides/getting-started-testing#enrichment-via-quarkustestcallback[documentation].
[source,java]
----
@QuarkusTest
@TestProfile(SimpleTest.class) //necessary only if "newly created" context is required for the test (worse performance)
public class SimpleTest extends CamelQuarkusTestSupport {
...
}
----