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
- JUnit Parameterized Test Runner
- YAML
- Jersey Client
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