Testing

Testing offers a good way to ensure camel routes behave as expected over time. Before going deeper into the subject, it is strongly advised to read First Steps and Quarkus testing.

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.

CamelTestSupport style testing can also be used. Note that it can only work in JVM mode.

A test running in JVM mode

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:

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

@QuarkusTest
class MyTest {
    @Test
    void test() {
        // Use any suitable code that send test data to the route and then assert outcomes
    }
}

An example implementation can be found here.

A test running in native mode

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.

import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest
class MyIT extends MyTest {
}

An example implementation of a native can be found here.

@QuarkusTest Vs @QuarkusIntegrationTest

JVM mode tests annotated with @QuarkusTest are executed in the same JVM as the application under test. This makes it possible to @Inject CDI beans from the application into the test code. You can also define new beans or even override beans from the application using @jakarta.enterprise.inject.Alternative and @jakarta.annotation.Priority.

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.

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:

  • 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.

  • 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 Quarkus testing guide.

Testing with external services

Testcontainers

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, Testcontainers 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 (host, port, user, password, etc. of the remote service) to the application before it starts. 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.

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(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.getHost());
                put("my-container.port", "" + container.getMappedPort(1234));
        }};
    }

    @Override
    public void stop() {
        // Stop the needed container(s)
        myContainer.stop();
    }
}

The defined test resource needs to be referenced from the test classes with @WithTestResource as shown below:

import io.quarkus.test.common.WithTestResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
@WithTestResource(MyTestResource.class)
class MyTest {
   ...
}

More information can be found in the Quarkus testing guide. Refer to Camel Quarkus source tree for a 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 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 Twilio.

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 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()
                    .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 @WithTestResource 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.

import io.quarkus.test.common.WithTestResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
@WithTestResource(WireMockTestResource.class)
class MyTest {
   ...
}

More examples of WireMock usage can be found in the Camel Quarkus integration test source tree such as in the validator tests.

CamelTestSupport style of testing

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.

CamelQuarkusTestSupport only works in JVM mode. If you need to test in native mode, then use one of the alternate test strategies described above.

To use CamelQuarkusTestSupport, you must add camel-quarkus-junit5 as a test scoped dependency to your application.

<dependency>
    <groupId>org.apache.camel.quarkus</groupId>
    <artifactId>camel-quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>

Customizing the CamelContext for testing

You can customize the CamelContext for testing with configuration profiles, CDI beans, observers, mocks etc. You can also override the createCamelContext method and interact directly with the CamelContext.

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.
@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:

@QuarkusTest
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 test profile, which will force the application under test to be restarted.

For example, to define a test profile:

@QuarkusTest
class MyTestProfile implements QuarkusTestProfile {
    ...
}

Then reference it on the test class with @TestProfile:

// @TestProfile will trigger the application to be restarted
@TestProfile(MyTestProfile.class)
@QuarkusTest
class SimpleTest extends CamelQuarkusTestSupport {
    ...
}
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:

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:

@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

@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 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

    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

    @QuarkusTest
    class SimpleTest extends CamelQuarkusTestSupport {
        @BindToRegistry("myBean")
        MyBean myBean = new MyBean();
    }
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.