Data-Driven RESTful API Testing for Java

Posted by Dave on 4 February 2015

This post is a Zombie that I'm resurrecting from my drafts. I"m not doing any Java these days, but hopefully this post might be useful to somebody

In my quest to get better code coverage for the OVSDB project in OpenDaylight I started to look at increasing coverage for the REST API. It's pretty difficult to test this in an efficient way (lines of code) and frameworks like Robot would have been easier to use. The disadvantage with using an external test framework is that code coverage (using a plugin like JaCoCo) would not be logged. Therefore I harnessed my Junit-Jitsu and found a solution that lives in the JVM

The Scenario

Lets take a very simple example REST API

GET, PUT: /v2/foo

Step 1: The Solution Components

The solution uses the following components

The parameterized runner will run run a test multiple times given a bunch of parameters. This way we can write one test, specifiy our parameters in YAML and let JUnit do the hard work!

Step 2: Writing the YAML file

Here's a sample YAML file:

---
- name: testGetAllFoo
    operation: GET
    uri: /v2/foo
    json:
    expected: 200

- name: testCreateBadFoo
    operation: PUT
    uri: /v2/foo
    json:
    expected: 400

- name: testCreateFoo
    operation: PUT
    uri: /v2/foo
    json: >
    {
        "foo": {
            "bar": 0,
            "baz": "abc",
            "quux": ["d", "e", "f"]
        }
    }
    expected: 200

When given to JUnit, this will run 3 tests.

Step 3: Writing the Test

package com.example.api;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.yaml.snakeyaml.Yaml;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter

/* Constants */
public static final String USERNAME = "admin";
public static final String PASSWORD = "admin";
public static final String BASE_URI = "http://localhost:8080";
public static final String MEDIA_TYPE_JSON = "application/json";

/* This annotation tell JUnit to do it's parameterized magic */
@RunWith(Parameterized.class)
public class ApiTest{

    /* Here we can name our tests, although I never got this working properly */
    @Parameters(name = "ApiTest{index}:{0}")
    public static List<Object[]> getData() throws FileNotFoundException {
        /* Load the file from the resources directory */
	    ClassLoader classloader = ApiTest.class.getClassLoader();
        InputStream input = classloader.getResourceAsStream("northbound.yaml");
        
        /* Parse the YAML */
        Yaml yaml = new Yaml();
        List<Map<String, Object>> object = (List<Map<String, Object>>) yaml.load(input);
        List<Object[]> parameters = Lists.newArrayList();

        for (Map<String, Object> o : object){
            Object[] l = o.values().toArray();
            parameters.add(l);
        }
        
        return parameters;

    }

    /* These are the parameters from the YAML  */
    private String fName;
    private String fOperation;
    private String fPath;
    private String fJson;
    private int fExpected;
 

    /* Parameters are passed to the constructor */
    public ApiTest(String name, String operation, String path, String json, int expected){
        fName = name;
        fOperation = operation;
        fPath = path;
        fJson = json;
        fExpected = expected;
    }
    
    @Test
    public void testCase() {
        System.out.println("Running " + fName + "...\n");

        /* Create an HTTP Client */
        Client client = Client.create();
        client.addFilter(new HTTPBasicAuthFilter(USERNAME , PASSWORD));
        String uri = BASE_URI + fPath;
        WebResource webResource = client.resource(expand(uri));
        ClientResponse response = null;

        /* Send the right request according to operation parameter */
        switch (fOperation) {
            case "GET":
                response = webResource.accept(MEDIA_TYPE_JSON)
                        .get(ClientResponse.class);
                break;
            case "POST":
                response = webResource.accept(MEDIA_TYPE_JSON)
                        .header("Content-Type", MEDIA_TYPE_JSON)
                        .post(ClientResponse.class, expand(fJson));
                break;
            case "PUT":
                response = webResource.accept(MEDIA_TYPE_JSON)
                        .header("Content-Type", MEDIA_TYPE_JSON)
                        .put(ClientResponse.class, fJson);
                break;
            case "DELETE":
                response = webResource.delete(ClientResponse.class);
                break;
            default:
                fail("Unsupported operation");
        }
        /* Check the status code */
        assertEquals(fExpectedStatusCode, response.getStatus());
    }

    @Before
    public void setUp(){
        /* This bit is up to you. Make sure your web server is listening */
    }

}

Step 4: Profit

Adding additional tests is as simple as adding more lines in YAML! Running the tests however is another topic, but let's assume you already have enough time and patience to have already got Maven and Surefire working :)

For those interested in seeing this live, the full code from OpenDaylight can be found here.

@dave_tucker