Writing unit tests for Apache Camel routes using AdviceWith
I have migrated a Spring Boot Camel application to Quarkus Camel and found out that the unit tests required reworking due to the differences in CDI between Spring Boot and Quarkus.
The heavily reliance of Spring Boot annotations such as
@SpringBootTest
(for bootstraping the application context),@DirtiesContext
(for context modification),@TestPropertySource
(for configuring property files location),@MockBean
and@Autowire
increases the migration process complexity. While these CDI annotations simplify testing within the Spring Boot framework, they hide behaviours that make the testing very couple to Spring Boot framework. When migratin to other framworks (like Quarkus), this hidden magic often become at a price, requiring developers to spend more time understanding the hidden behaviour and implementing it in a more Java compliant way.
The main advantages of migrating from spring-boot to quarkus are that quarkus has a faster boot time, smaller, artifacts, and can easily be deployed into a native application (though this isn't necessary for 99% of use cases). Anyway, I don't want to dive deep into this kind of discussion, so I recommend this blog article from Baeldung where they do an experiment and share their findings.
Writing tests
I prefer to keep everything as simple as possible when writing unit tests. I don't like using 'magic' annotations like @Inject
or @Bean
. I always try to write explicit unit tests. By 'explicit,' I mean defining what will be mocked, how the mocks are injected into the testable class, what the provided input and expected output are, and ensuring that validation rules are set for each test.
In Camel, routes should be implemented to be short and focused, they shouldn’t describe the entire microservice process. The software engineer should be able to split the code into smaller routes because it will make testing much easier.
Regarding the testing class itself, I believe nobody should be using JUnit4 nowadays. Please use JUnit5 with Mockito to avoid creating wrapping classes of your classes in your tests. Some people use Wiremock to mock the API and simulate and test the API calls of your service which is quite useful in certain use cases.
CamelTestSupport
CamelTestSupport
is the essential class that you must extend. According to the documentation, it provides a variety of utilities to create the context and the template.
A useful base class that creates a
CamelContext
with some routes along with aProducerTemplate
for use in the test case. - Javadoc.
When your test class extends CamelTestSupport
, it will provide two important methods createRouteBuilder
, for instantiating your testing route, and startCamelContext
, for explicitly defining the camel context to be used during the unit test. By defining the camel context I mean, mocking the routes. The behavior of each mock will be set later on each unit test. The following code snippet demonstrates what was described in this paragraph.
Examplet
public class MyRouteTest extends CamelTestSupport {
@Override
protected RouteBuilder createRouteBuilder() {
// instantiate the route by injecting the processor and the mapper
// you can mock the processor and the mapper
return new MyRoute(MyProcessor.class, MyMapper.class);
}
@Override
protected void startCamelContext() throws Exception {
// get the first from("direct:route-name")
RouteDefinition route = context.getRouteDefinitions().get(0);
// AdviceWith will allow to replace the to() endpoints by point them to mock endpoints.
// This is also powerful because it allows the developer to mock enrich()
AdviceWith.adviceWith(route, context, new AdviceWithRouteBuilder() {
@Override
public void configure() {
weaveByToString("enrich*").replace().to("mock:myEnrichRoute");
weaveByToUri("direct:outputRoute").replace().to("mock:direct:outputRoute");
// or you can just remove them
weaveById("direct:outputRoute2").remove();
}
});
// without this line, the unit test will fail because you need to run the camel context
super.startCamelContext();
}
@Test
void testSuccesfulPath() throws InterruptedException {
// apply some behaviour to the mock route
getMockEndpoint("mock:direct:outputRoute")
.whenAnyExchangeReceived(exchange -> {
// the exchange will be returned with HEADER_EXAMPLE updated
exchange.getIn().setHeader(HEADER_EXAMPLE, true);
});
// applies a validation rule that at least one message should be processed by this mock route
getMockEndpoint("mock:direct:outputRoute").expectedMessageCount(1);
// there are basic methods to start the route, for instance template.sendBody() or template.sendBodyAndHeader()
// I suggest the usage of template.send() if you want to assert the output exchange class
template.setDefaultEndpointUri("direct:my-route");
template.sendBodyAndHeader(new BodyDTO(), HEADER_EXAMPLE, false);
// validates if the validation rules are satisfied
MockEndpoint.assertIsSatisfied(context);
}
}