Pragmatic Unit Testing, Part 2

4 Integration Testing REST Services

Now, we’ll look at using junit to do integration testing of a full REST service.  This will work much like functional tests in that we’ll first test creating resources, then test the rest of the CRUD operations by operating on such created resources.  But first some preliminaries.

4.1 AbstractApiTest

All the REST API tests are going to subclass the abstract class AbstractApiTest.  It is annotated just like the example test classes above, with the service’s API implementation injected by Spring.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={TestConfiguration.class})
@WebAppConfiguration
public abstract class AbstractApiTest {
    // the object under test
    @Inject
    protected ResourceApiImpl api;

    ………
}

4.2 UriInfo

To support HATEOAS, the ResourceApiImpl likely has this declaration in it:

@Context
private UriInfo uriInfo;

This structure is injected by the JaxRS infrastructure (Rest Easy in the examples below) which isn’t initialized in the test environment.  So AbstractApiTest has some code to manually inject it with reflection, as follows:

protected UriInfo uriInfoForCreateResource() throws URISyntaxException {
    return new ResteasyUriInfo(new URI("http://localhost:8080/v1/service"),
                               new URI("/resources"));
}

protected UriInfo uriInfoForReadResources() throws URISyntaxException {
    return new ResteasyUriInfo(new URI("http://localhost:8080/v1/service"),
                               new URI("/resources"));
}

protected UriInfo uriInfoForReadResource(String id) 
        throws URISyntaxException {
    return new ResteasyUriInfo(new URI("http://localost:8080/v1/service"),
                               new URI("/resources/"+id));
}

protected void injectUriInfo(UriInfo uriInfo) 
        throws NoSuchFieldException,
               SecurityException,
               IllegalArgumentException,
               IllegalAccessException {
    Field uriInfoField = ResourceApiImpl.class.getDeclaredField("uriInfo");
    uriInfoField.setAccessible(true);
    uriInfoField.set(api, uriInfo);
}

This is essentially a mock of the upstream environment of the service.

4.3 TrackingId and Logging

Each of my service calls gets passed an optional tracking id as a query parameter.  A random one will be generated by the service if one is not provided.  This id is reported in my logging.  Since I’m letting Spring do its Injection in my unit tests, my unit tests also do logging, so I fill in the tracking id of each operation with the name of the test plus a unique identifier.  This lets me consult my logs when a unit test goes wrong.  This has proven to be a great productivity booster when unit tests fail.  The details are left as an exercise for the reader.

4.4 Boilerplate For CRUD Operations

Each test is going to be a series of CRUD operations, usually chained together and operating on a single Resource object.  To this end, the following member variables exist in AbstractApiTest to hold state while the CRUD operations are being performed:

// these are filled in by the test before calling a CRUD utility
protected Resource postedResource;    // Resource to POST to create one
protected List<Patch> patches;        // List of patches to PATCH

// these are filled in by a CRUD utility
protected Response response;          // Response from CRUD service
protected Resource responseResource;  // The resource returned in the response from the CRUD service
protected String resourceId;          // The primary key that was assigned to a created resource
protected List<String> etags;         // Etags for race detection

// this is filled in by the test to compare the actual responseResource against
protected Resource expectedResource;  // The Resource we expect to get back from the CRUD service

These get used by CRUD utilities as follows:

protected void createResource() throws Exception {
    injectUriInfo(uriInfoForCreateResource());
    response = api.createResource(postedResource, trackingId());
    responseResource = (Resource)response.getEntity();
    etags = response.getHeaders().get("Etag").stream()
            .map(Object::toString)
            .collect(Collectors.toList());
    resourceId = responseResource.getId();
}

protected void patchResource() throws Exception {
    injectUriInfo(uriInfoForReadResource(resourceId));
    response = api.patchResource(patches, resourceId, trackingId(), etags);
    responseResource = (Resource)response.getEntity();
    etags = response.getHeaders().get("Etag").stream()
            .map(Object::toString)
            .collect(Collectors.toList());       
}

protected void readResource() throws Exception {
    injectUriInfo(uriInfoForReadResource(resourceId));
    response = api.readResource(resourceId, trackingId());
    responseResource = (Resource)response.getEntity();
    etags = response.getHeaders().get("Etag").stream()
            .map(Object::toString)
            .collect(Collectors.toList());
}

………

4.5 testCreateResource()

Now we can write an actual test to create a resource.  The details of validateResponse() method will be discussed further down below, but suffice it to know that it compares responseResource to expectedResource with the appropriate asserts for now.  Keep in mind, these tests as shown are just illustrations.  A real test would probably have some sort of data provider that provide numerous postedResource/expectedResource pairs.  Other details are left as an exercise for the reader.

public class CreateResourceTest extends AbstractApiTest {
    @Test
    public void testCreateResource() throws Exception {
        // used to generate a tracking id for logging
        testName = new Object(){}.getClass().getEnclosingMethod().getName();

        // set up the test resource to post/perform the operation under test
        postedResource = new Resource.Builder()
                .resourceType(ResourceType.PERSON.name())
                .startPersonDetails()
                    .name("Abraham Lincoln")
                    .birthdate("1809-02-12")
                .endPersonDetails()
                .build();
        createResource();

        // set up the expected results
        expectedResource = new Resource.Builder()
                .id(resourceId)
                .resourceType(ResourceType.PERSON.name())
                .startPersonDetails()
                    .name("Abraham Lincoln")
                    .birthdate("1809-02-11")
                .endPersonDetails()
                .build();

        // validate the response against the expected results
        validateResponse(HttpURLConnection.HTTP_CREATED);

        // don’t let this test’s name bleed over to a test that forgets
        // to set testName
        testName = null;
    }
}

4.6 TestPatchResource()

Additional operations can be tested by building upon testCreateResource’s foundation.

public class CreateResourceTest extends AbstractApiTest {
    @Test
    public void testCreateResource() throws Exception {
        // used to generate a tracking id for logging
        testName = new Object(){}.getClass().getEnclosingMethod().getName();

        // create a resource to perform the test operation on
        postedResource = new Resource.Builder()
                .resourceType(ResourceType.PERSON.name())
                .startPersonDetails()
                    .name("Abraham Linkoln")  // misspelling!
                    .birthdate("1809-02-11")  // wrong birthdate!
                .endPersonDetails()
                .build();
        createResource();

        // set up a patch/perform the operation under test
        patches = new Patch.ListBuilder()
                .startPatch()
                    .op("replace")
                    .path("/person_details/name")
                    .value("Abraham Lincoln")
                .endPatch()
                .startPatch()
                    .op("replace")
                    .path("/person_details/birthdate")
                    .value("1809-02-12")
                .endPatch()
                .build();
        patchResource();

        // set up the expected results
        expectedResource = new Resource.Builder()
                .id(resourceId)
                .resourceType(ResourceType.PERSON.name())
                .startPersonDetails()
                    .name("Abraham Lincoln")
                    .birthdate("1809-02-11")
                .endPersonDetails()
                .build();

        // validate the response against the expected results
        validateResponse(HttpURLConnection.HTTP_OK);

        // don’t let this test’s name bleed over to a test that forgets
        // to set testName
        testName = null;
    }
}

4.7 Why Do This?

Everything here is about productivity.  Tests that can be run quickly and debugged easily will make the developer more productive.  Greater productivity means the developer can spend more time refactoring the code for elegance, writing comments, writing more tests, thinking about the design, reviewing code, drinking coffee, reading Facebook, etc.

Because unit tests don’t require complex staging environments, they can reliably be run without the developer having to inevitably stop and figure out what broken downstream component has been installed on the staging environment today.

When tests inevitably fail, they can be debugged without having to set up remote debugging and connecting to two different processes (the test and the service) to track down the problem.

When small changes to implementation don’t require changes to dozens of mocks, the time spent can be spent insuring the quality of the code in other ways.

When tests don’t become silently broken because their mocks are constantly being hacked on, bugs don’t make it to live and suck up the entire team’s productivity trying to track it down quickly in overtime and shipping an emergency release.

5 Response Validation Framework

This is something I glossed over somewhat above and that I have a number ideas to improve upon.  At some future point in time, I’ll write a blog entry on just this topic.

What I’m doing now is constructing a Resource with the expected results, except that the value of any String field is actually a regular expression.  This works great for String fields, but not so much for fields that are a numeric type.  What I want to do instead, but haven’t implemented yet, is write something that looks like a Builder, but does validation instead.  So it might end up looking something like this:

new ResourceValidator()
        .id("(?<id>[0-9]){1,8}")
        .resourceType(ResourceType.PERSON.name())
        .startPersonDetails()
            .name("Abraham Lincoln")
            .birthdate("1809-02-11")
        .endPersonDetails()
        .startLinks()
            .startLink()
                .href("http://localhost:8080/v1/service/resources/(?<id>[0-9]){1,8}")
                .rel("self")
            .endLink()
        .endLinks()
        .validate(responseResource);

In this example, “id” might be an integer field in Resource, but we’re matching the expected value with a regular expression.  Note the use of named groups to specify that the same value should appear in more than one place in the response.  The string representation of the integer “id” also appears in the HATEOAS links.

6 Functional Tests?  Left as an Exercise for the Reader

A direction for future experiments would be to see if I could generate both unit tests and functional tests from the same source.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: