This is an automated email from the ASF dual-hosted git repository. jamesnetherton pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push: new 67758a7b31 Improve the testing guide 67758a7b31 is described below commit 67758a7b31a055cede4e0bca1b064aae8772422d Author: James Netherton <jamesnether...@gmail.com> AuthorDate: Tue Jun 25 14:35:50 2024 +0100 Improve the testing guide * Fix grammar * Reword some sentences for brevity * Explain some of the core differences CamelTestSupport & CamelQuarkusTestSupport testing * Add some usage examples for CamelQuarkusTestSupport * Improve the limitations section for better clarity --- docs/modules/ROOT/pages/user-guide/testing.adoc | 342 +++++++++++++++++++----- 1 file changed, 274 insertions(+), 68 deletions(-) diff --git a/docs/modules/ROOT/pages/user-guide/testing.adoc b/docs/modules/ROOT/pages/user-guide/testing.adoc index 947f14ed5a..2ad31d57bf 100644 --- a/docs/modules/ROOT/pages/user-guide/testing.adoc +++ b/docs/modules/ROOT/pages/user-guide/testing.adoc @@ -2,19 +2,17 @@ :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]. +Before going deeper into 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. +When it comes to testing a route in the context of Quarkus, the recommended approach is to write local integration tests. This has the advantage of covering both JVM and native mode. -The xref:#_cameltestsupport_style_of_testing[`CamelTestSupport` approach] can be used since Camel Quarkus 2.12.0. Note that it can only work in JVM mode. - -Let's enumerate some points of interest below. +xref:#_cameltestsupport_style_of_testing[`CamelTestSupport`] style testing can also be used. Note that it can only work in JVM mode. == 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: +All tests should be annotated with `@QuarkusTest`. This will bootstrap Quarkus and start Camel routes before the `@Test` logic is executed, +like in the example below: [source,java] ---- @@ -25,9 +23,8 @@ import org.junit.jupiter.api.Test; @QuarkusTest class MyTest { @Test - public void test() { + void test() { // Use any suitable code that send test data to the route and then assert outcomes - ... } } ---- @@ -37,11 +34,11 @@ An example implementation can be found https://github.com/apache/camel-quarkus/b [[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. +Providing all extensions your application depends on are supported in native mode, +you should test that your application works correctly native mode. +The test logic defined in JVM mode can be reused in native mode, by inheriting from the respective JVM mode class. +The `@QuarkusIntegrationTest` annotation instructs the Quarkus JUnit extension to compile the application under test to a native image +and will start it before running the tests. [source,java] ---- @@ -49,35 +46,31 @@ 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]. +An example implementation of a native can be found 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` +== `@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 `@jakarta.enterprise.inject.Alternative` and `@jakarta.annotation.Priority`. +This makes it possible to https://quarkus.io/guides/getting-started-testing#injection-into-tests[`@Inject`] CDI beans from the application into the test code. +You can also define new beans or even override beans from the application using https://quarkus.io/guides/getting-started-testing#cdi-alternative-mechanism[`@jakarta.enterprise.inject.Alternative`] and https://quarkus.io/guides/getting-started-testing#cdi-alternative-mechanism[`@jakarta.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. +These options do not work in native mode for tests annotated with `@QuarkusIntegrationTest`, as they 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. +An important consequence of this, is that all communication between the tests and the native application, +must take one or more of the following forms: -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. +* Network calls. Typically, HTTP or any other network protocol your application supports. +* Watching the filesystem for changes (E.g via Camel `file` endpoints). +* 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. +* 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]. @@ -86,16 +79,17 @@ For more information about `QuarkusIntegrationTest` see the https://quarkus.io/g === Testcontainers -Sometimes your application needs to access some external service, such as messaging broker, database, etc. +Sometimes your application needs to access an external service such as a message 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. +can be used to start and stop these services for testing. -For the application to work properly it is often essential to pass the connection configuration data +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 +In the Quarkus ecosystem, `QuarkusTestResourceLifecycleManager` serves this purpose. + +You can start one or more containers in the `start()` method +and return the connection configuration in the form of a `Map`, where the key is the name of a configuration property and the value is the property value. +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. @@ -110,20 +104,21 @@ import org.testcontainers.containers.wait.strategy.Wait; public class MyTestResource implements QuarkusTestResourceLifecycleManager { - private GenericContainer myContainer; + private GenericContainer<?> myContainer; @Override public Map<String, String> start() { // Start the needed container(s) - myContainer = new GenericContainer(...) + myContainer = new GenericContainer(DockerImageName.parse("my/image:1.0.0")) .withExposedPorts(1234) .waitingFor(Wait.forListeningPort()); myContainer.start(); // Pass the configuration to the application under test + // You can also pass camel component property names / values to automatically configure Camel components return new HashMap<>() {{ - put("my-container.host", container.getContainerIpAddress()); + put("my-container.host", container.getHost()); put("my-container.port", "" + container.getMappedPort(1234)); }}; } @@ -132,11 +127,11 @@ public class MyTestResource implements QuarkusTestResourceLifecycleManager { 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: +The defined test resource needs to be referenced from the test classes with `@QuarkusTestResource` as shown below: [source,java] ---- @@ -150,7 +145,7 @@ 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]. +More information can be found in the https://quarkus.io/guides/getting-started-testing#quarkus-test-resource[Quarkus testing guide]. Refer to Camel Quarkus source tree for a https://github.com/apache/camel-quarkus/blob/main/integration-tests/google-storage/src/test/java/org/apache/camel/quarkus/component/google/storage/it/GoogleStorageTestResource.java[complete example]. === WireMock @@ -188,8 +183,8 @@ public class WireMockTestResource implements QuarkusTestResourceLifecycleManager ); server.start(); - // Stub a HTTP endpoint. Note that WireMock also supports a record and playback mode - // http://wiremock.org/docs/record-playback/ + // Stub an HTTP endpoint. Note that WireMock also supports a record and playback mode + // https://wiremock.org/docs/record-playback/ server.stubFor( get(urlEqualTo("/api/greeting")) .willReturn(aResponse() @@ -226,14 +221,16 @@ 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]. +More examples of WireMock usage can be found in the Camel Quarkus integration test source tree such as in the https://github.com/apache/camel-quarkus/blob/main/integration-tests/validator/src/test/java/org/apache/camel/quarkus/component/validator/it/ValidatorTestResource.java[validator tests]. == `CamelTestSupport` style of testing -If you used plain Camel before, you may know `CamelTestSupport` already. -Unfortunately the Camel variant won't work on Quarkus, so we prepared a replacement called `CamelQuarkusTestSupport`, which can be used in JVM mode. +If you used Camel standalone or on other runtimes before, you may know `CamelTestSupport` already. +The original `CamelTestSupport` class is not well suited to Quarkus, so there's an extended Quarkus friendly version called `CamelQuarkusTestSupport`. + +> IMPORTANT: `CamelQuarkusTestSupport` only works in JVM mode. If you need to test in native mode, then use one of the alternate test strategies described above. -Please add following dependency into your module (preferably in the `test` scope), to use `CamelQuarkusTestSupport`. +To use `CamelQuarkusTestSupport`, you must add `camel-quarkus-junit5` as a test scoped dependency to your application. [source,xml] ---- @@ -244,31 +241,240 @@ Please add following dependency into your module (preferably in the `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] for 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`) and JUnit Jupiter annotations (`@BeforeEach`, `@AfterEach`, `@AfterAll` and `@BeforeAll`) might not work correctly. See the https://quarkus.io/guides/getting-started-testing#enrichment-via-quarkustestcallback[documentation]. -* If there is an unaffected route, when using advice with, it's important to execute method `CamelQuarkusTestSupport.startRouteDefinitions()` manually from the unit test after you are done doing all the advice with -* Do not use `@Produces` with `RouteBuilder` use the overridden method `createRouteBuilder()` instead. `@Produces` with `RouteBuilder` might not work correctly. -* Please use `quarkus.camel.routes-discovery.exclude-patterns` and/or `quarkus.camel.routes-discovery.include-patterns` to configure which routes from the application (`src/main/java`) are used during the tests execution. See more details in the xref:reference/extensions/core.adoc[documentation]. +=== Customizing the `CamelContext` for testing + +You can customize the `CamelContext` for testing with https://quarkus.io/guides/config-reference#profiles[configuration profiles], CDI beans, observers, https://quarkus.io/guides/getting-started-testing#mock-support[mocks] etc. +You can also override the `createCamelContext` method and interact directly with the `CamelContext`. + +IMPORTANT: When using `createCamelContext` you *MUST NOT* instantiate and return a new `CamelContext`. Instead, invoke `super.createCamelContext()` and modify the returned `CamelContext` as needed. +Failing to follow this rule will result in an exception being thrown. + +[source,java] +---- +@QuarkusTest +class SimpleTest extends CamelQuarkusTestSupport { + + @Override + protected CamelContext createCamelContext() throws Exception { + // Must call super to get a handle on the application scoped CamelContext + CamelContext context = super.createCamelContext(); + // Apply customizations + context.setTracing(true); + // Return the modified CamelContext + return context; + } +} +---- + +=== Configuring routes for testing + +Any classes that extend `RouteBuilder` in your application will have their routes automatically added to the `CamelContext`. +Similarly, any XML or YAML routes configured from `camel.main.routes-include-pattern` will also be loaded. + +This may not always be desirable for your tests. +You control which routes get loaded at test time with configuration properties: + +* `quarkus.camel.routes-discovery.include-patterns` +* `quarkus.camel.routes-discovery.exclude-patterns`, +* `camel.main.routes-include-pattern` +* `camel.main.routes-exclude-pattern`. + +You can also define test specific routes per test class by overriding `createRouteBuilder`: [source,java] ---- @QuarkusTest -@TestProfile(SimpleTest.class) //necessary only if "newly created" context is required for the test (worse performance) -public class SimpleTest extends CamelQuarkusTestSupport { +class SimpleTest extends CamelQuarkusTestSupport { + @Test + void testGreeting() { + MockEndpoint mockEndpoint = getMockEndpoint("mock:result"); + mockEndpoint.expectedBodiesReceived("Hello World"); + + template.sendBody("direct:start", "World"); + + mockEndpoint.assertIsSatisified(); + } + + @Override + protected RoutesBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:start") + .transform().simple("Hello ${body}") + .to("mock:result"); + } + }; + } +} +---- + +=== CamelContext test lifecycle + +One of the main differences in `CamelQuarkusTestSupport` compared to `CamelTestSupport` is how the `CamelContext` lifecycle is managed. + +On Camel Quarkus, a single `CamelContext` is created for you automatically by the runtime. +By default, this `CamelContext` is shared among all tests and remains started for the duration of the entire test suite execution. + +This can potentially have some unintended side effects for your tests. +If you need to have the `CamelContext` restarted between tests, then you can create a custom https://quarkus.io/guides/getting-started-testing#testing_different_profiles[test profile], which will force the application under test to be restarted. + +For example, to define a test profile: + +[source,java] +---- +@QuarkusTest +class MyTestProfile implements QuarkusTestProfile { ... } ---- +Then reference it on the test class with `@TestProfile`: + +[source,java] +---- +// @TestProfile will trigger the application to be restarted +@TestProfile(MyTestProfile.class) +@QuarkusTest +class SimpleTest extends CamelQuarkusTestSupport { + ... +} +---- + +NOTE: You cannot manually restart the `CamelContext` by invoking its `stop()` and `start()` methods. This will result in an exception. + +=== Examples + +==== Simple `RouteBuilder` and test class + +Simple `RouteBuilder`: + +[source,java] +---- +public class MyRoutes extends RouteBuilder { + @Override + public void configure() { + from("direct:start") + .transform().simple("Hello ${body}") + .to("mock:result"); + } +} +---- + +Test sending a message payload to the `direct:start` endpoint: + +[source,java] +---- +@QuarkusTest +class SimpleTest extends CamelQuarkusTestSupport { + @Test + void testGreeting() { + MockEndpoint mockEndpoint = getMockEndpoint("mock:result"); + mockEndpoint.expectedBodiesReceived("Hello World"); + + template.sendBody("direct:start", "World"); + + mockEndpoint.assertIsSatisified(); + } +} +---- + +==== Using `AdviceWith` + +[source,java] +---- +@QuarkusTest +class SimpleTest extends CamelQuarkusTestSupport { + @BeforeEach + public void beforeEach() throws Exception { + AdviceWith.adviceWith(this.context, "advisedRoute", route -> { + route.replaceFromWith("direct:replaced"); + }); + } + + @Override + protected RoutesBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:start").routeId("advisedRoute") + .transform().simple("Hello ${body}") + .to("mock:result"); + } + }; + } + + @Test + void testAdvisedRoute() throws Exception { + MockEndpoint mockEndpoint = getMockEndpoint("mock:result"); + mockEndpoint.expectedBodiesReceived("Hello World"); + + template.sendBody("direct:replaced", "World"); + + mockEndpoint.assertIsSatisfied(); + } +} +---- + +==== Explicitly enabling advice + +When explicitly xref:manual::advice-with.adoc#_enabling_advice_during_testing[enabling advice] you must invoke `startRouteDefinitions` when completing your `AdviceWith` setup. +Note that this is only required if you have routes configured that are NOT being advised. + +=== Limitations + +==== Test lifecycle methods inherited from `CamelTestSupport` + +`CamelQuarkusTestSupport` inherits some test lifecycle methods from `CamelTestSupport`. However, they should not be used and instead are replaced with equivalent methods in `CamelQuarkusTestSupport`. + +|=== +|CamelTestSupport lifecycle methods |CamelQuarkusTestSupport equivalent + +|`afterAll` +|`doAfterAll` + +|`afterEach`, `afterTestExecution` +|`doAfterEach` + +|`beforeAll` +|`doAfterConstruct` + +|`beforeEach` +|`doBeforeEach` +|=== + +==== Creating a custom Camel registry is not supported + +The `CamelQuarkusTestSupport` implementation of `createCamelRegistry` will throw `UnsupportedOperationException`. + +If you need to bind or unbind objects to the Camel registry, then you can do it by one of the following methods. + +* Produce named CDI beans ++ +[source,java] +---- +public class MyBeanProducers { + @Produces + @Named("myBean") + public MyBean createMyBean() { + return new MyBean(); + } +} +---- ++ +* Override `createCamelContext` (see example above) and invoke `camelContext.getRegistry().bind("foo", fooBean)` +* Use the `@BindToRegistry` annotation ++ +[source,java] +---- +@QuarkusTest +class SimpleTest extends CamelQuarkusTestSupport { + @BindToRegistry("myBean") + MyBean myBean = new MyBean(); +} +---- ++ ++ + +NOTE: Beans bound to the Camel registry from individual test classes, will persist for the duration of the test suite execution. +This could have unintended consequences, depending on your test expectations. You can use test profiles to restart the `CamelContext` to avoid this.