Friday, January 14, 2011

Test Your CXF REST Service by Examining Application Object State

While I've previously described using CXF to unit test a REST service, it's a limited technique since the object returned from the HTTP request is a response string - and any assertions you could make against that string are going to be brittle, since it relies on the decisions around how the encoding (XML or JSON) is done. What we really want is a more object-oriented mechanism for testing against application object state...which means you need a REST client that calls the service and can then look at the returned application object, as opposed to a Response object.

Now, the CXF website describes just such an approach - but I was reluctant to go down that road, since I recalled trying it before (some time ago) and just giving up after many hours of fruitless effort. So I developed my own approach, but to be absolutely fair about things, I decided to first revisit the CXF approach before posting this article - just some due diligence. Here's what I found - first, here's my REST resource:


@Path("/mystuff/")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public interface ItemResource {
@GET
@Path("{item}")
// the UriInfo is used to RESTfully expose the URI that navigates to item details
Item getItem(@Context UriInfo uriInfo, @PathParam("item") String itemName);
}

Given this, I try the various approaches as per the CXF article - please read the internal comments for my insights:


@Test(enabled = false)
public void tryCXFRecipesForClientAccess() throws Exception {

// first try the proxy approach
MyCollection collection = JAXRSClientFactory.create(
"http://localhost:8080/mystuff", MyCollection.class);

// my API calls for a UriInfo object, but I don't have one, so I try just using null:
Item item = collection.getItem(null, "item1");
// no can do; error message is "getItem expects JAXRS Context parameter which is
// not supported on the client side". I don't want to spend time solving this
// when there might not even be a solution; nor do I want to change my REST API
// just to get a test to work...

// ...so I try the WebClient approach instead, again as per the example code in
// the CXF article - but, this next line won't even compile:
WebClient client = WebClient.create(service);
// ...the docs say use the create(Object) method, but apparently that's not right

// next, with a little digging, I find an alternate API:
WebClient webclient = WebClient.create("http://localhost:8080/mystuff");
Item item = webclient.get(Item.class);
// no luck: "unexpected element (uri:"", local:"Items"). Expected elements are <{}Items>"
}

At this point, I bail out - that's as much due diligence as I'm willing to give. If I were doing research or some university course project, I'd persevere; but I'm in a production environment - under time constraints - and as such, now I feel justified in using my homegrown technique. Here's what this will buy me:

  1. As was true with my first testing approach, I'll be able to start up my REST service without deployment to a container, i.e. as a standalone, which will facilitate testing against it
  2. Though I'll engage the service by sending requests with a vanilla HTTP client, I'll gain the ability to examine the application object that is returned instead of just a Response string

These things are accomplished with no changes needed for the production service. The moving parts of the solution include:

  1. The production REST service, both interface and implementation
  2. A CXF wrapper for standalone deployment
  3. An alternate implementation of the REST interface used solely for testing
  4. Spring configuration 

 Here's the recipe:

  • The REST service interface returns application-level objects, as you can see from the ItemResource class above
  • The CXF wrapper accepts the interface in its constructor, and configures things from there:

public class StandaloneRestService {

private JAXRSServerFactoryBean sf;
public StandaloneRestService(ItemResource resource) {
sf = new JAXRSServerFactoryBean();
sf.setResourceClasses(ItemResource.class);

sf.setResourceProvider(ItemResource.class, new SingletonResourceProvider(resource));
sf.setAddress("http://localhost:9001/mystuff");

sf.create();
}
}
  • The alternate REST implementation also accepts the interface in its constructor, using compose-and-forward for method implementations, saving the returned application object and providing a protected getter for that cached object:

public class TestableItemResource implements ItemResource {

private ItemResource resource;
private Item item;

public TestableItemResource(ItemResource theResource) {
resource = theResource;
}

@Override
public Item getItem(UriInfo uriInfo, String itemName) {
this.item = resource.getItem(uriInfo, itemName);
return this.item;
}

protected Item getResultItem() {
return this.item;
}
}
  • Spring constructs the production service, the test service and the CXF wrapper, wiring up the constructor arguments as needed - in a file named e.g. spring-beans.xml:

<bean id="itemResource" class="com.mybiz.ItemResourceImpl"/>
<bean id="testableItemResource" class="com.mybiz.TestableItemResource">
<constructor-arg ref="itemResource"/>
</bean>
<bean id="cxfServer" class="com.mybiz.StandaloneRestService">
<constructor-arg ref="testableItemResource"/>
</bean>
  • The test class (using testNG) specifies the Spring config, extends a Spring test base class to get access to the application context, then saves an instance field of the test service implementation:

@ContextConfiguration(locations = "spring-beans.xml")
public class ItemRestApiTest extends AbstractTestNGSpringContextTests {

private TestableItemResource restSvc;

@BeforeMethod(enabled = true)
public void saveRestService() throws Exception {
restSvc = this.applicationContext.getBean(TestableItemResource.class);
assert restSvc != null;
}
  • This test class makes HTTP calls in its tests, then checks the state of application objects by calling those protected getters in the test service:

new URL("http://localhost:9001/mystuff/" + itemName).openStream();
Item item = restSvc.getResultItem();
// ... now you can test against the expected state of the item...

No comments:

Post a Comment