Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Developing Services with Apache Camel - Part II: Creating and Testing Routes

DZone's Guide to

Developing Services with Apache Camel - Part II: Creating and Testing Routes

· Java Zone
Free Resource

Microservices! They are everywhere, or at least, the term is. When should you use a microservice architecture? What factors should be considered when making that decision? Do the benefits outweigh the costs? Why is everyone so excited about them, anyway?  Brought to you in partnership with IBM.

This article is the second in a series on Apache Camel and how I used it to replace IBM Message Broker for a client. The first article, Developing Services with Apache Camel - Part I: The Inspiration, describes why I chose Camel for this project.

To make sure these new services correctly replaced existing services, a 3-step approach was used:

  1. Write an integration test pointing to the old service.
  2. Write the implementation and a unit test to prove it works.
  3. Write an integration test pointing to the new service.

I chose to start by replacing the simplest service first. It was a SOAP Service that talked to a database to retrieve a value based on an input parameter. To learn more about Camel and how it works, I started by looking at the CXF Tomcat Example. I learned that Camel is used to provide routing of requests. Using its CXF component, it can easily produce SOAP web service endpoints. An end point is simply an interface, and Camel takes care of producing the implementation.

Legacy Integration Test

I started by writing a LegacyDrugServiceTests integration test for the old drug service. I tried two different ways of testing, using WSDL-generated Java classes, as well as using JAX-WS's SOAP API. Finding the WSDL for the legacy service was difficult because IBM Message Broker doesn't expose it when adding "?wsdl" to the service's URL. Instead, I had to dig through the project files until I found it. Then I used the cxf-codegen-plugin to generate the web service client. Below is what one of the tests looked like that uses the JAX-WS API.

@Test
public void sendGPIRequestUsingSoapApi() throws Exception {
    SOAPElement bodyChildOne = getBody(message).addChildElement("gpiRequest", "m");
    SOAPElement bodyChildTwo = bodyChildOne.addChildElement("args0", "m");
    bodyChildTwo.addChildElement("NDC", "ax22").addTextNode("54561237201");
    SOAPMessage reply = connection.call(message, getUrlWithTimeout(SERVICE_NAME));
    if (reply != null) {
        Iterator itr = reply.getSOAPBody().getChildElements();
        Map resultMap = TestUtils.getResults(itr);
        assertEquals("66100525123130", resultMap.get("GPI"));
    }
}

Implementing the Drug Service

In the last article, I mentioned I wanted no XML in the project. To facilitate this, I used Camel's Java DSL to define routes and Spring'sJavaConfig to configure dependencies.

The first route I wrote was one that looked up a GPI (Generic Product Identifier) by NDC (National Drug Code).

@WebService
public interface DrugService {
    @WebMethod(operationName = "gpiRequest")
    GpiResponse findGpiByNdc(GpiRequest request);
}

To expose this as a web service endpoint with CXF, I needed to do two things:

  1. Tell Spring how to configure CXF by importing "classpath:META-INF/cxf/cxf.xml" into a @Configuration class.
  2. Configure CXF's Servlet so endpoints can be served up at a particular URL.

To satisfy item #1, I created a CamelConfig class that extends CamelConfiguration. This class allows Camel to be configured by Spring's JavaConfig. In it, I imported the CXF configuration, allowed tracing to be configured dynamically, and exposed myapplication.properties to Camel. I also set it up (with @ComponentScan) to look for Camel routes annotated with @Component.

@Configuration
@ImportResource("classpath:META-INF/cxf/cxf.xml")
@ComponentScan("com.raibledesigns.camel")
public class CamelConfig extends CamelConfiguration {
    @Value("${logging.trace.enabled}")
    private Boolean tracingEnabled;
 
    @Override
    protected void setupCamelContext(CamelContext camelContext) throws Exception {
        PropertiesComponent pc = new PropertiesComponent();
        pc.setLocation("classpath:application.properties");
        camelContext.addComponent("properties", pc);
        // see if trace logging is turned on
        if (tracingEnabled) {
            camelContext.setTracing(true);
        }
        super.setupCamelContext(camelContext);
    }
 
    @Bean
    public Tracer camelTracer() {
        Tracer tracer = new Tracer();
        tracer.setTraceExceptions(false);
        tracer.setTraceInterceptors(true);
        tracer.setLogName("com.raibledesigns.camel.trace");
        return tracer;
    }
}

CXF has a servlet that's responsible for serving up its services at common path. To map CXF's servlet, I leveraged Spring'sWebApplicationInitializer in an AppInitializer class. I decided to serve up everything from a /api/* base URL.

package com.raibledesigns.camel.config;
 
import org.apache.cxf.transport.servlet.CXFServlet;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
 
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
 
public class AppInitializer implements WebApplicationInitializer {
 
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        servletContext.addListener(new ContextLoaderListener(getContext()));
        ServletRegistration.Dynamic servlet = servletContext.addServlet("CXFServlet", new CXFServlet());
        servlet.setLoadOnStartup(1);
        servlet.setAsyncSupported(true);
        servlet.addMapping("/api/*");
    }
 
    private AnnotationConfigWebApplicationContext getContext() {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.setConfigLocation("com.raibledesigns.camel.config");
        return context;
    }
}

To implement this web service with Camel, I created a DrugRoute class that extends Camel's RouteBuilder.

@Component
public class DrugRoute extends RouteBuilder {
    private String uri = "cxf:/drugs?serviceClass=" + DrugService.class.getName();
 
    @Override
    public void configure() throws Exception {
        from(uri)
            .recipientList(simple("direct:${header.operationName}"));
        from("direct:gpiRequest").routeId("gpiRequest")
            .process(new Processor() {
                public void process(Exchange exchange) throws Exception {
                    // get the ndc from the input
                    String ndc = exchange.getIn().getBody(GpiRequest.class).getNDC();
                    exchange.getOut().setBody(ndc);
                }
            })
            .to("sql:{{sql.selectGpi}}")
            .to("log:output")
            .process(new Processor() {
                public void process(Exchange exchange) throws Exception {
                    // get the gpi from the input
                    List<HashMap> data = (ArrayList<HashMap>) exchange.getIn().getBody();
                    DrugInfo drug = new DrugInfo();
                    if (data.size() > 0) {
                        drug = new DrugInfo(String.valueOf(data.get(0).get("GPI")));
                    }
                    GpiResponse response = new GpiResponse(drug);
                    exchange.getOut().setBody(response);
                }
            });
    }
}

The sql.selectGpi property is read from src/main/resources/application.properties and looks as follows:

sql.selectGpi=select GPI from drugs where ndc = #?dataSource=ds.drugs

The "ds.drugs" reference is to a datasource that's created by Spring. From my AppConfig class:

@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig {
 
    @Value("${ds.driver.db2}")
    private String jdbcDriverDb2;
 
    @Value("${ds.password}")
    private String jdbcPassword;
 
    @Value("${ds.url}")
    private String jdbcUrl;
 
    @Value("${ds.username}")
    private String jdbcUsername;
 
    @Bean(name = "ds.drugs")
    public DataSource drugsDataSource() {
        return createDataSource(jdbcDriverDb2, jdbcUsername, jdbcPassword, jdbcUrl);
    }
 
    private BasicDataSource createDataSource(String driver, String username, String password, String url) {
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName(driver);
        ds.setUsername(username);
        ds.setPassword(password);
        ds.setUrl(url);
        ds.setMaxActive(100);
        ds.setMaxWait(1000);
        ds.setPoolPreparedStatements(true);
        return ds;
    }
}

Unit Testing

The hardest part about unit testing this route was figuring out how to use Camel's testing support. I posted a question to the Camel users mailing list in early June. Based on advice received, I bought Camel in Action, read chapter 6 on testing and went to work. I wanted to eliminate the dependency on a datasource, so I used Camel's AdviceWith feature to modify my route and intercept the SQL call. This allowed me to return pre-defined results and verify everything worked.

@RunWith(CamelSpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = CamelSpringDelegatingTestContextLoader.class, classes = CamelConfig.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@UseAdviceWith
public class DrugRouteTests {
 
    @Autowired
    CamelContext camelContext;
 
    @Produce
    ProducerTemplate template;
 
    @EndpointInject(uri = "mock:result")
    MockEndpoint result;
 
    static List<Map> results = new ArrayList<Map>() {{
        add(new HashMap<String, String>() {{
            put("GPI", "123456789");
        }});
    }};
 
    @Before
    public void before() throws Exception {
        camelContext.setTracing(true);
 
        ModelCamelContext context = (ModelCamelContext) camelContext;
        RouteDefinition route = context.getRouteDefinition("gpiRequest");
        route.adviceWith(context, new RouteBuilder() {
            @Override
            public void configure() throws Exception {
                interceptSendToEndpoint("sql:*").skipSendToOriginalEndpoint().process(new Processor() {
                    @Override
                    public void process(Exchange exchange) throws Exception {
                        exchange.getOut().setBody(results);
                    }
                });
            }
        });
        route.to(result);
        camelContext.start();
    }
 
    @Test
    public void testMockSQLEndpoint() throws Exception {
        result.expectedMessageCount(1);
        GpiResponse expectedResult = new GpiResponse(new DrugInfo("123456789"));
        result.allMessages().body().contains(expectedResult);
 
        GpiRequest request = new GpiRequest();
        request.setNDC("123");
        template.sendBody("direct:gpiRequest", request);
 
        MockEndpoint.assertIsSatisfied(camelContext);
    }
}

I found AdviceWith to be extremely useful as I developed more routes and tests in this project. I used its weaveById feature to intercept calls to stored procedures, replace steps in my routes and remove steps I didn't want to test. For example, in one route, there was a complicated workflow to interact with a customer's data.

  1. Call a stored procedure in a remote database, which then inserts a record into a temp table.
  2. Lookup that data using the value returned from the stored procedure.
  3. Delete the record from the temp table.
  4. Parse the data (as CSV) since the returned value is ~ delimited.
  5. Convert the parsed data into objects, then do database inserts in a local database (if data doesn't exist).

To make matters worse, remote database access was restricted by IP address. This meant that, while developing, I couldn't even manually test from my local machine. To solve this, I used the following:

  • interceptSendToEndpoint("bean:*") to intercept the call to my stored procedure bean.
  • weaveById("myJdbcProcessor").before() to replace the temp table lookup with a CSV file.
  • Mockito to mock a JdbcTemplate that does the inserts.

To figure out how to configure and execute stored procedures in a route, I used the camel-store-procedure project on GitHub. Mockito'sArgumentCaptor also became very useful when developing a route that called a 3rd-party web service within a route. James Carr has more information on how you might use this to verify values on an argument.

To see if my tests were hitting all aspects of the code, I integrated the cobertura-maven-plugin for code coverage reports (generated by running mvn site).

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>cobertura-maven-plugin</artifactId>
            <configuration>
                <instrumentation>
                    <excludes>
                        <exclude>**/model/*.class</exclude>
                        <exclude>**/AppInitializer.class</exclude>
                        <exclude>**/StoredProcedureBean.class</exclude>
                        <exclude>**/SoapActionInterceptor.class</exclude>
                    </excludes>
                </instrumentation>
                <check/>
            </configuration>
            <version>2.6</version>
        </plugin>
...
<reporting>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>cobertura-maven-plugin</artifactId>
            <version>2.6</version>
        </plugin>

Integration Testing

Writing an integration test was fairly straightforward. I created a DrugRouteITest class, a client using CXF's JaxWsProxyFactoryBean and called the method on the service.

public class DrugRouteITest {
 
    private static final String URL = "http://localhost:8080/api/drugs";
 
    protected static DrugService createCXFClient() {
        JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
        factory.setBindingId("http://schemas.xmlsoap.org/wsdl/soap12/");
        factory.setServiceClass(DrugService.class);
        factory.setAddress(getTestUrl(URL));
        return (DrugService) factory.create();
    }
 
    @Test
    public void findGpiByNdc() throws Exception {
        // create input parameter
        GpiRequest input = new GpiRequest();
        input.setNDC("54561237201");
 
        // create the webservice client and send the request
        DrugService client = createCXFClient();
        GpiResponse response = client.findGpiByNdc(input);
 
        assertEquals("66100525123130", response.getDrugInfo().getGPI());
    }
}

This integration test is only run after Tomcat has started and deployed the app. Unit tests are run by Maven's surefire-plugin, while integration tests are run by the failsafe-plugin. An available Tomcat port is determined by the build-helper-maven-plugin. This port is set as a system property and read by the getTestUrl() method call above.

public static String getTestUrl(String url) {
    if (System.getProperty("tomcat.http.port") != null) {
        url = url.replace("8080", System.getProperty("tomcat.http.port"));
    }
    return url;
}

Below are the relevant bits from pom.xml that determines when to start/stop Tomcat, as well as which tests to run.

<plugin>
    <groupId>org.apache.tomcat.maven</groupId>
    <artifactId>tomcat7-maven-plugin</artifactId>
    <version>2.2</version>
    <configuration>
        <path>/</path>
    </configuration>
    <executions>
        <execution>
            <id>start-tomcat</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <fork>true</fork>
                <port>${tomcat.http.port}</port>
            </configuration>
        </execution>
        <execution>
            <id>stop-tomcat</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>shutdown</goal>
            </goals>
        </execution>
    </executions>
</plugin>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.17</version>
    <configuration>
        <excludes>
            <exclude>**/*IT*.java</exclude>
            <exclude>**/Legacy**.java</exclude>
        </excludes>
        <includes>
            <include>**/*Tests.java</include>
            <include>**/*Test.java</include>
        </includes>
    </configuration>
</plugin>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>2.17</version>
    <configuration>
        <includes>
            <include>**/*IT*.java</include>
        </includes>
        <systemProperties>
            <tomcat.http.port>${tomcat.http.port}</tomcat.http.port>
        </systemProperties>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

The most useful part of integration testing came when I copied one of my legacy tests into it and started verifying backwards compatibility. Since we wanted to replace existing services, and require no client changes, I had to make the XML request and response match. Charles was very useful for this exercise, letting me inspect the request/response and tweak things to match. The following JAX-WS annotations allowed me to change the XML element names and achieve backward compatibility.

  • @BindingType(SOAPBinding.SOAP12HTTP_BINDING)
  • @WebResult(name = "return", targetNamespace = "...")
  • @ResponseWrapper(localName = "gpiResponse")
  • @WebParam(name = "args0", targetNamespace = "...")
  • @XmlElement(name = "...")

Continuous Integration and Deployment

My next item of business was configuring a job in Jenkins to continually test and deploy. Getting all the tests to pass was easy, and deploying to Tomcat was simple enough thanks to the Deploy Plugin and this article. However, after a few deploys, Tomcat would throw OutOfMemory exceptions. Therefore, I ended up creating a second "deploy" job that stops Tomcat, copies the successfully-built WAR to $CATALINA_HOME/webapps, removes $CATALINA_HOME/webapps/ROOT and restarts Tomcat. I used Jenkins "Execute shell" feature to configure these three steps. I was pleased to find my /etc/init.d/tomcat script still worked for starting Tomcat at boot time and providing convenient start/stop commands.

Summary

This article shows you how I implemented and tested a simple Apache Camel route. The route described only does a simple database lookup, but you can see how Camel's testing support allows you to mock results and concentrate on developing your route logic. I found its testing framework very useful and not well documented, so hopefully this article helps to fix that. In the next article, I'll talk about upgrading to Spring 4, integrating Spring Boot and our team's microservice deployment discussions.


Discover how the Watson team is further developing SDKs in Java, Node.js, Python, iOS, and Android to access these services and make programming easy. Brought to you in partnership with IBM.

Topics:

Published at DZone with permission of Matt Raible, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}