Recipe: Unit testing Apache CXF RESTful services

· Read in about 4 min · (780 words) ·

Recently, decided to use Apache CXF to expose a service with a RESTful API. Part of the reason for choosing REST had more to do with the fact that the client is going to be a mobile client. These days, though mobile devices stacks have come a long way and provide SOAP clients, it still seems prudent to not depend on a whole slew of technologies where plain 'ole HTTP and JSON might do the trick. As I started exploring CXF, I liked the JAX-RS implementation and decided to go ahead with it - however, almost immediately, hit a snag when I went on to write test cases. Apache CXF documentation is not quite there and things do require some investigation - at least initially till you get a hang of the framework. As it took time to figure out the solution, it makes sense to share it on blogosphere. Here’s how to go about writing unit tests:

Firstly, the service and the service implementation:

package com.aditi.blackberry.web;

import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;

@Path("/chat")
@Produces("application/json")
public interface ChatWebService \{

@POST
@Path("connect")
public Response connect(@FormParam("user")String username,
@FormParam("pass")String password);
}

The service implementation:

package com.aditi.blackberry.web;

import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

@Produces("application/json")
public class ChatWebServiceImpl implements ChatWebService {

    public Response connect(String username, String password) {
    if(username ==null || "".equals(username) ||
        password ==null || "".equals(password)) {
        return Response.status(Status.BAD_REQUEST).build();
    }
    String[] response = {username, password};
    return Response.ok(response).build();
    }
}

The corresponding spring context xml (applicationContext.xml) is:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:jaxrs="http://cxf.apache.org/jaxrs" xmlns:cxf="http://cxf.apache.org/core"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
       http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
       http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd">

	<import resource="classpath:META-INF/cxf/cxf.xml" />
	<import resource="classpath:META-INF/cxf/cxf-extension-jaxrs-binding.xml" />

	<bean id="logInbound" class="org.apache.cxf.interceptor.LoggingInInterceptor" />
	<bean id="logOutbound" class="org.apache.cxf.interceptor.LoggingOutInterceptor" />
	<!--
     logging
     -->
	<cxf:bus>
		<cxf:inInterceptors>
			<ref bean="logInbound" />
		</cxf:inInterceptors>
		<cxf:outInterceptors>
			<ref bean="logOutbound" />
		</cxf:outInterceptors>
		<cxf:inFaultInterceptors>
			<ref bean="logOutbound" />
		</cxf:inFaultInterceptors>
	</cxf:bus>
	<jaxrs:server id="jaxrsservice" address="${server.address}" >
		<jaxrs:serviceBeans>
			<ref bean="chatwebservice" />
		</jaxrs:serviceBeans>
		<jaxrs:providers>
			<ref bean="flexjsonprovider"/>
		</jaxrs:providers>
	</jaxrs:server>
	<bean id="flexjsonprovider" class="com.raghu.junitcxf.FlexJsonMessageBodyWriter" />
	<bean id="chatwebservice" class="com.raghu.junitcxf.ChatWebServiceImpl" />
</beans>

A few things to note here - logging is turned on using interceptors and the jaxrs server is defined. I’m also using flexJson to convert arbitrary objects to json - so a MessageBodyWriter bean is also injected into the jaxrs server node. The most important thing is that we havent included either the cxf-servlet.xml config for the cxf-extension-http-jetty.xml. Essentially, what we want to do is for the actual build, include cxf-servlet.xml and for the test runs, run the service on the bundled jetty server.

So, go ahead and define a applicationContext-web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:jaxrs="http://cxf.apache.org/jaxrs" xmlns:cxf="http://cxf.apache.org/core"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
       http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
       http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd">
<bean id="propertyConfigurer"
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="location" value="classpath:/build.properties" />
	</bean>
	<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />
	<import resource="classpath:/applicationContext.xml" />

</beans>

This is the context xml that we’ll provide to the ContextLoaderListener in our web.xml.

For the test cases, define applicationContext-test.xml - this is the context xml which we’ll load from the test cases.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:jaxrs="http://cxf.apache.org/jaxrs" xmlns:cxf="http://cxf.apache.org/core"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
       http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
       http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd">
	<bean id="propertyConfigurer"
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="location" value="classpath:/test.properties" />
	</bean>
	<import resource="classpath:META-INF/cxf/cxf-extension-http-jetty.xml" />
	<import resource="classpath:/applicationContext.xml" />
	<jaxrs:client id="chatclient" address="${server.address}" serviceClass="com.raghu.junitcxf.ChatWebService">
		<jaxrs:headers>
			<entry key="Accept" value="application/json"></entry>
		</jaxrs:headers>
	</jaxrs:client>
</beans>

As you see, we also define a jaxrs:client for the test context xml.

There’s one final issue to address - which is that we would ideally like the urls we use to access the service to be the same. The spring jaxrs:server binding takes an address attribute which defines the url the service is hosted on. For deployment onto an external container, this takes the form of "/myservice" - a path element relative to the context location. For the internal jetty hosted service, it takes the full http path (http://localhost:port/my/path/to/service). The easiest way is to have this set using a property reference in spring and have the applicationContext-web.xml and applicationContext-test.xml load different property files as shown in above.

For completeness, here’s the web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<display-name>CXF REST Example</display-name>
	<description>CXF REST Example</description>

	<context-param>
		<param-name>webAppRootKey</param-name>
		<param-value>cxf.rest.example.root</param-value>
	</context-param>
<!--
	<context-param>
		<param-name>log4jConfigLocation</param-name>
		<param-value>/WEB-INF/classes/log4j.properties</param-value>
	</context-param>

	<listener>
		<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
	</listener>
 -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:/applicationContext-web.xml</param-value>
	</context-param>

	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<servlet>
		<servlet-name>CXFServlet</servlet-name>
		<servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>CXFServlet</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>

</web-app>

And finally, here’s a junit test case:

base class:

    package com.aditi.blackberry.web;

    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.test.context.ContextConfiguration;
    import
    org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = { "classpath:/applicationContext-test.xml" })
    public abstract class AbstractApiTest {

        @Autowired
        @Qualifier("chatclient")
        protected ChatWebService proxy;
    }

A test case for the connect API:

package com.aditi.blackberry.web;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.junit.Assert;
import org.junit.Test;

public class ConnectApiTest extends AbstractApiTest\{
    @Test
    public void testConnect() \{
        Response resp = proxy.connect("raghu", "password");
        Assert.assertTrue(resp.getStatus() == 200);
        System.out.println(resp.getEntity().toString());
    }
}