Testing routes defined in Spring

This recipe expands on the core testing capabilities described so far by detailing the steps needed to test Camel routes defined using the XML DSL in a Spring application. You will learn how to assemble a test harness that replaces parts of the application in order to test your routes outside a deployment environment, including the substitution of Spring ${..} placeholders with test values.

Getting ready

The Java code for this recipe is located in the org.camelcookbook.examples.testing.spring package. The Spring XML files are located under src/main/resources/META-INF/spring.

To use Camel's Spring test support, you need to add a dependency for the camel-test-spring library that provides the support classes for JUnit testing of Spring as well as a transitive dependency on JUnit itself.

Add the following to the dependencies section of your Maven POM:

<dependency>
  <groupId>org.apache.camel</groupId>
  <artifactId>camel-test-spring</artifactId>
  <version>${camel-version}</version>
  <scope>test</scope>
</dependency>

How to do it...

Consider the following route, defined in the file located in /META-INF/spring/simpleTransform-context.xml:

<route>
  <from uri="direct:in"/>
  <transform>
    <simple>Modified: ${body}</simple>
  </transform>
  <log message="Set message to ${body}"/>
  <to uri="mock:out"/>
</route>

To test this route, perform the following steps:

  1. Create a test class that extends org.apache.camel.test.spring.CamelSpringTestSupport:
    public class SimpleTransformSpringTest 
        extends CamelSpringTestSupport {
      //...
    }

    The CamelSpringTestSupport class is an abstract class that is responsible for instantiating the Camel context with the routes under test, creating utility objects, and injecting annotated properties into the test.

  2. Override the createApplicationContext() method from CamelSpringTestSupport and instantiate a Spring application context that loads the files containing the Camel routes under test:
    @Override
    protected AbstractApplicationContext
        createApplicationContext() {
      return new ClassPathXmlApplicationContext(
          "/META-INF/spring/simpleTransform-context.xml");
    }
  3. Define the body of the test. Use the MockEndpoint testing DSL to set expectations about the message you plan to receive during testing. A message is sent into the route via a ProducerTemplate instance provided for you by the CamelTestSupport class. Finally, assert that the expectations set on the mock endpoint were satisfied:
    @Test
    public void testPayloadIsTransformed() 
        throws InterruptedException {
      MockEndpoint mockOut = getMockEndpoint("mock:out");
      mockOut.setExpectedMessageCount(1);
      mockOut.message(0).body().isEqualTo("Modified: Cheese");
    
      template.sendBody("direct:in", "Cheese");
    
      assertMockEndpointsSatisfied();
    }

    As an alternative to explicitly fetching mocks, and referring to endpoints in each unit test, you can request an autowired MockEndpoint and ProducerTemplate. These are defined as bean properties, and annotated as follows:

    @EndpointInject(uri = "mock:out")
    private MockEndpoint mockOut;
    
    @Produce(uri = "direct:in")
    private ProducerTemplate in;

How it works...

The CamelSpringTestSupport class is a convenience class that provides feature-parity with the CamelTestSupport class described in the Testing routes defined in Java recipe. It is responsible for performing the boilerplate work required to test Camel routes defined within Spring configuration files. At its most basic, the class will do the following:

  • Start a Spring application defined by the context returned from createApplicationContext() before each test.
  • Inject any properties that you have annotated with @Produce, @EndpointInject, or Spring's @Autowired. The last one allows your test code to get a handle on any object defined within the Spring application.
  • Shut down the Spring application at the end of each test.

Feature-parity with CamelTestSupport means that aside from the implementation of a different base method to the Java testing example (createApplicationContext() versus createRouteBuilder() or createRouteBuilders()), CamelSpringTestSupport allows the test methods themselves to be written in exactly the same manner as their Java DSL equivalents. Both classes provide access to the same protected variables (context and template), and honor the same test lifecycle.

The testing method described has a drawback—it requires that a Camel base class is extended. This may not be something that you would like if you want to use another class as the base for your tests. To cater for this, Camel provides what is known as the Enhanced Spring Test option.

To make use of this, your test uses the JUnit 4 Runner functionality that allows a custom class to manage the lifecycle of a test. The rewritten test class appears as follows:

@RunWith(CamelSpringJUnit4ClassRunner.class)
@ContextConfiguration({
    "/META-INF/spring/simpleTransform-context.xml"})
public class SimpleTransformSpringRunnerTest {
  //...
}

As no base class is extended, your tests no longer have access to the protected variables, or utility methods such as assertMockEndpointsSatisfied() that are provided by CamelSpringTestSupport. The workaround for this is fairly straightforward. The Camel context is injected via the @Autowired annotation, just as any other Spring object:

@Autowired
private CamelContext camelContext;

When using Enhanced Spring Tests you invoke the static MockEndpoint.assertIsSatisfied() utility method to assert that all the mock endpoints in the Camel context interface were satisfied:

@Test
public void testPayloadIsTransformed() 
    throws InterruptedException {
  mockOut.setExpectedMessageCount(1);
  mockOut.message(0).body().isEqualTo("Modified: Cheese");

  producerTemplate.sendBody("Cheese");

  MockEndpoint.assertIsSatisfied(camelContext);
}

Note

One thing to watch out for is that the Enhanced Spring Tests have a different test lifecycle than those extending CamelSpringTestSupport. Instead of setting up and tearing down the entire Spring application between tests, by default, the Spring application is set up once only at the start of the test class. This impacts the mock endpoints in classes with more than one test method, as these are not reset to their initial state between tests.

To get around this, add the following class annotation to the test:

@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD)

There's more...

Spring applications are usually broken up into multiple context files that address a particular fragment of an application each—you would generally see one file per horizontal application tier, or per vertical stack, of functionality. By making use of this approach, you can substitute alternative Spring configuration files when assembling your tests to replace portions of an application. This allows you to stub out objects used in your routes that may be difficult to wire up in a test environment.

It is usually a good idea to keep your PropertyPlaceholderConfigurer configuration in a different Spring XML file from your routes. This allows you to plug in a test version of those properties for testing.

We can use this particular mechanism to fully externalize our route endpoints, and use dependency injection to make the route more easily testable. Consider the following example:

<camelContext xmlns="http://camel.apache.org/schema/spring">
  <propertyPlaceholder id="properties" 
                       location="ref:ctx.properties"/>
  <route>
    <from uri="{{start.endpoint}}"/>
    <transform>
      <simple>Modified: ${body}</simple>
    </transform>
    <log message="Set message to ${body}"/>
    <to uri="{{end.endpoint}}"/>
  </route>
</camelContext>

By providing an alternative PropertyPlaceholderConfigurer instance–defined as a bean with an id of ctx.properties–in a test file in a test file, we can exercise the route using direct: and mock: endpoints, while referring to the actual technology endpoints in the production configuration.

Note

Using this approach of substituting in test endpoints, you are validating the logic of the route, and not the behavior of the route with the actual components. This may give you a false sense of security, as the routing logic may not work in the same way when the real endpoints are in use. For example, a consumer endpoint may use a different type of object in the exchange body than what your substituted test endpoint provides, and this will subtlety change your logic.

This sort of testing should always be used in conjunction with an integration test that exercises the real endpoints.

The following example uses the Spring util and context namespaces for brevity to substitute test endpoints into the preceding route:

<util:properties id="ctx.properties"
                 location="classpath:spring/test.properties"/>
<context:property-placeholder properties-ref="ctx.properties" />

The test.properties file contains the test versions of the properties:

start.endpoint=direct:in
end.endpoint=mock:out

Splitting out the PropertyPlaceholderConfigurer configuration into a different file is particularly useful if the file containing your Camel context definition also contains other beans that need properties injected.

Tip

It is a good practice to place any beans that have complex dependencies, or could be considered services, in a separate configuration file to your routes. This makes it easy to provide an alternative version of those beans in your test by providing a different version of that file to the test runtime.

You can alternatively feed properties directly into the context through the Properties component by overriding the following method from CamelTestSupport:

@Override
protected Properties 
    useOverridePropertiesWithPropertiesComponent() {
  Properties properties = new Properties();
  properties.setProperty("start.endpoint", "direct:in");
  properties.setProperty("end.endpoint", "mock:out");
  return properties;
}

If your propertyPlaceholder refers to properties files that are not discoverable at test time, you can instruct the framework to ignore the error instead of throwing an exception that would prevent the Camel context from starting up. You do this by overriding the following method in your test:

@Override
protected Boolean ignoreMissingLocationWithPropertiesComponent() {
  return true;
}

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.46.141