Wednesday, August 26, 2009

Recipe: Unit testing Apache CXF RESTful services

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:

[sourcecode language="java"]
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);
}
[/sourcecode]

The service implementation:

[sourcecode language="java"]
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();
}
}
[/sourcecode]

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

[sourcecode language="xml"]

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">





























[/sourcecode]

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:

[sourcecode language="xml"]
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">




[/sourcecode]

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.

[sourcecode language="xml"]
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">









[/sourcecode]

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:

[sourcecode language="xml"]

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">

CXF REST Example
CXF REST Example


contextConfigLocation
classpath:/applicationContext-web.xml


org.springframework.web.context.ContextLoaderListener



CXFServlet
org.apache.cxf.transport.servlet.CXFServlet
1



CXFServlet
/*



[/sourcecode]

And finally, here's a junit test case:

base class:

[sourcecode language="java"]
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;
}
[/sourcecode]

A test case for the connect API:

[sourcecode language="java"]
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());
}
}
[/sourcecode]

21 comments :

  1. Hi Raghu great tutorial. Best so for on the subject. Can you post the full code? I seem to be having problems with the providers. I can seem to make it work.

    Thanks

    Boni

    ReplyDelete
  2. Very good tutorial. Much appreciated.

    ReplyDelete
  3. Very good example. is there any place I can download the project.

    ReplyDelete
  4. Really nice ! As Sanjay Benerjee, a way to download the projet ?

    ReplyDelete
  5. @Sanjay, @David and others

    Really sorry about the source - I'd written this up quite some time ago and have changed machines thrice since then and dont have the source :( :( on my current machine.

    I'll look around some more to see if I can find it - and if so will put it up on github or something.

    ReplyDelete
  6. [...] the original post on the topic written about two and a half years ago had code snippets, but there’s been comments and PMs [...]

    ReplyDelete
  7. @Sanjay, @David and others who wanted the code - http://github.com/raghur/cxf-junit-jetty/

    http://niftybits.wordpress.com/2011/12/30/unit-testing-apache-cxf-restful-services-code-available/

    ReplyDelete
  8. Many thanks for the code !

    I've one small question, which web.xml file the embedded jetty will use ? Let's assume that you are using the open session in View pattern with Hibernate, defined in the web.xml (as servlet filter). It seems that with your configuration, this setup is not taking into account. Any idea ?

    Thanks

    ReplyDelete
  9. web.xml wont get loaded at all when you run the test cases - so yeah - if you defined something in your web.xml, that never happens.

    Also, the open session in view is just a wrapper around your Session - and while web.xml filter is one way, its by no means the only way. Here's a spring reference interceptor you can use:

    http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.html

    ReplyDelete
  10. For some reason the embedded Jetty starts up and just waits at the port. The tests don't detect that jetty is ready somehow and never invoke any requests. I end up getting a timeout. Any idea why this is happening?

    ReplyDelete
  11. Just want to thank you.
    Really useful recipe.
    Helped me a lot.

    ReplyDelete
  12. I was wondering, do you know of anyway to attach debugging to the embedded Jetty instance running for these tests, or maybe if the Jetty service logs somewhere? I'm having trouble debugging tests that I've set up like this. My test expects a 201 for example, but gets a 500 with no response, I have no insight into what happened on the server.

    Any tips would be GREAT!

    ReplyDelete
  13. Don't you see the logs in the eclipse window (if in the IDE) or on the console when running with mvn?

    You could also debug the test from eclipse which will allow you to step through code on the Eclipse Debug Perspective...

    ReplyDelete
  14. No I wasn't seeing anything in the IDE console from the server side (embedded Jetty). I did find something that i added to the VM args that now is showing the logs now from Jetty so I do see the logs now. But the debugger will not attach to the embedded Jetty instance because I believe it's running in a different VM, not in the same one the IDE is running in. So to debug you'd have to either stop the forking of the Jetty instance into a different VM (not sure how to do this) or attach as a remote debugging process, which I don't know how to do.

    ReplyDelete
  15. Hi,

    I have just setup a similar environment for testing REST service but have encounter an issue :
    "Connection refused connect"

    It seems to me that Jetty is not started anywhere (as per the logs of Spring).

    So I am confused.
    Is this recipe really supposed to work standalone and to start a Jetty server itself ?
    Or do I need to instantiate a Jetty server myself ?

    Is this line supposed to launch a Jetty server ?


    My purpose is to be able to launch a pseudo-integration test, standalone, without having to deploy my application on a real application server.

    Do you have any hint/resource on it ?
    Any help would be appreciated.

    Thanks

    ReplyDelete
  16. @Raphael Jolivet

    Yes - it should start embedded Jetty. You shouldn't need to do anything else other than mvn test. Looks like the line you posted is missing/got stripped off.

    If you don't see Jetty starting up, do you have the right dependencies on the test project on the CXF jetty runtime?


    org.apache.cxf
    cxf-rt-transports-http-jetty
    ${cxf.version}


    Have you cloned the repo and tried running that? It does exactly what you're looking for...

    ReplyDelete
  17. does this also go through CXF Request Filter?

    ReplyDelete
  18. If anyone is interested, these are the VM arguments for enabling the debug logs from jetty:

    -Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog -D{classref}.LEVEL=DEBUG

    ReplyDelete
  19. How much time did it require you to compose “Recipe:
    Unit testing Apache CXF RESTful services | Nifty tidbits”?
    It has quite a bit of decent material. Thanks -Angela

    ReplyDelete
  20. @Angela - don't remember - I did it when I started poking into CXF and took me a bit of time - probably a couple of days... Promptly wrote a post about it and forgot everything... but there were so many requests for code that I finally dug out my old svn repo where I had it and just pushed it up to github.

    ReplyDelete