Integration work sits at the boundary between systems. Routes coordinate APIs, message brokers, and services, turning independent components into working workflows.
To keep that coordination reliable, it helps to treat integration code like any other application code: with clear boundaries, well-defined contracts, and fast automated tests that verify behavior early and often. This allows teams to validate routing logic and transformations long before every external dependency is fully available.
This article walks through a practical approach for building and testing integration routes using Apache Camel, Spring, and JUnit.
Design integration code to be testable from the start
Integration applications are still applications. They process data, enforce business rules, and change system state. That means the same design-for-testability principles apply:
- Define expected behavior clearly.
- Write tests early, ideally before implementation.
- Implement to satisfy those tests.
- Refactor while tests protect behavior.
When these tests run in CI, they become durable regression protection as routes evolve.
Object lifecycles matter
- Singleton: created once per application runtime.
- Prototype: created per usage.
- Scoped: created and destroyed within a specific scope (for example, a web request).
Unmanaged objects are instantiated by the developer. Their lifecycle is under the control of the scope they exist within. Then they get garbage collected.
XmlProcessingService xmlProcessingService = new XmlProcessingService();
Managed objects
Managed objects are created and destroyed by the the dependency injection system. For example the Spring framework, provides declarative object composition through xml. This allows us to be explicit about object lifetime rules. The centralized our object graph into a single artifact.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:amq="http://activemq.apache.org/schema/core" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd"><bean id="xmlProcessingService" class= "com.mycompany.integration.project.services.XmlProcessingService" ></bean></beans>
Application structure
Our sample project integrates customer and order XML payloads and maps them into model classes generated from an XSD.

Code is organized by component type:
- Models for schema-derived structures.
- Services for focused domain processing behavior.
- Utilities for reusable helpers.
- Routes for pipeline orchestration.
Tests follow the same package convention as production code. This symmetry keeps ownership and coverage obvious as the project grows.

The Spring composition model follows the same role-oriented design.

Unit testing by component category
Testing utilities
Utility methods are generally easy to verify with simple black-box tests using the familiar Arrange–Act–Assert pattern.
package com.mycompany.integration.project.tests.utils;import org.junit.Test;import com.mycompany.integration.project.models.*;import com.mycompany.integration.project.utils.*;public class ModelBuilderTests {String xmlFilePath = "src/exemplar/CustomersOrders-v.1.0.0.xml";@Testpublic void test_can_fast_deserialize() throws Exception {// arrangeString xmlString = FileUtils.getFileString(xmlFilePath);// actCustomersOrders customersOrders = CustomersOrders.class.cast(ModelBuilder.fastDeserialize(xmlString,CustomersOrders.class));// assertassert (customersOrders != null);}@Testpublic void test_can_deserialize() throws Exception {// arrangeString axmlString = FileUtils.getFileString(xmlFilePath);// actCustomersOrders customersOrders = CustomersOrders.class.cast(ModelBuilder.deserialize(axmlString,CustomersOrders.class));// assertassert (customersOrders != null);}}
Testing domain services
Domain services are commonly tested with Spring’s test support:
- Load the required application context
- Inject the service under test
- Reset the context between tests to maintain isolation
Complete test example:
package com.mycompany.integration.project.tests.services;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.annotation.DirtiesContext;import org.springframework.test.annotation.DirtiesContext.ClassMode;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import com.mycompany.integration.project.services.XmlProcessingService;import com.mycompany.integration.project.utils.FileUtils;@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration( locations={"classpath:META-INF/spring/domain.xml"})@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)public class XmlProcessingServiceTests {String xmlFilePath = "src/exemplar/CustomersOrders-v.1.0.0.xml";@Autowiredprivate XmlProcessingService xmlProcessingService;@Testpublic void test_service_gets_injected() throws Exception {assert(xmlProcessingService != null);}@Testpublic void test_process_an_xml_transaction()throws Exception {// ArrangeString xml = FileUtils.getFileString(xmlFilePath);// ActBoolean result = xmlProcessingService.processTransaction(xml);// Assertassert(result);}}
Testing Camel routes defined in Spring DSL
For Camel route tests, the route itself is the system under test. The critical assertions are about:
- step sequencing,
- payload transformation,
- header correctness,
- message count expectations.
The key implementation detail is to create a dedicated test Camel context in src/test/resources that imports production routes and overrides external dependencies.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd"><!-- the Camel route is defined in another XML file --><import resource="classpath:META-INF/spring/camel-context.xml" /><!-- mock endpoint and override activemq bean with a noop stub (DirectComponent) --><bean id="mockAllEndpoints" class="org.apache.camel.impl.InterceptSendToMockEndpointStrategy"></bean><bean id="activemq" class="org.apache.camel.component.direct.DirectComponent"></bean></beans>
This isolates tests from infrastructure concerns, so routes can be validated without requiring live ActiveMQ or HTTP environments.
Route examples
<camelContext id="customers_and_orders_processing" xmlns="http://camel.apache.org/schema/spring"> <route id="process_messages_as_models"> <from uri="file:src/data1" /> <process ref="customersOrdersModelProcessor" id="process_as_model" /> <to uri="file:target/output1" /> </route> <route id="process_messages_as_xml"> <from uri="file:src/data2" /> <process ref="customersOrdersXmlDocumentProcessor" id="process_as_xml" /> <to uri="file:target/output2" /> </route> <route id="process_jetty_messages_as_xml"> <from uri="jetty:http://0.0.0.0:8888/myapp/myservice/?sessionSupport=true" /> <process ref="customersOrdersXmlDocumentProcessor" id="process_jetty_input_as_xml" /> <to uri="file:target/output3" /> <transform> <simple> OK </simple> </transform> </route></camelContext>
A useful maintenance convention is to align route IDs directly to test method names. This makes test reports self-explanatory and speeds up diagnosis.
Endpoint mocking convention
In Camel tests, endpoint mocking follows a simple pattern:
file:src/data1 → mock:file:src/data1
That convention allows deterministic message injection and assertion:
mockOutput.expectedBodiesReceived(inputExemplar);mockOutput.expectedHeaderReceived("status", "SUCCESS");mockOutput.expectedMessageCount(SEND_COUNT);
When assertions fail, messages are explicit and actionable.

Why this approach pays off
- Faster feedback: failures surface while coding, not after deployment.
- Safer change: route behavior is protected as collaborators evolve.
- Lower coupling: tests are not blocked by infrastructure availability.
- Continuous confidence: CI executes route checks on every build.
These are not tool-specific ideas. They are practical software design habits applied to integration architecture.
Conclusion
Testable routing is less about testing syntax and more about design discipline. Give routes clear intent, isolate boundaries, and verify behavior continuously. With that foundation, integration code becomes maintainable software rather than fragile glue.
If you want to explore the original implementation, the code is available here:

Leave a Reply