Philipp Hauer's Blog

Engineering Management, Java Ecosystem, Kotlin, Sociology of Software Development

Testing RESTful Services in Java: Best Practices

Posted on Mar 29, 2016. Updated on Jun 12, 2022

Testing RESTful Web Services can be cumbersome because you have to deal with low-level concerns which can make your tests verbose, hard to read and to maintain. Fortunately, there are libraries and best practices helping you to keep your integration tests concise, clean, decoupled and maintainable. This post covers those best practices.

Testing RESTful Services in Java: Best Practices

TL;DR

  • The general best practices for unit testing in Java also apply for testing RESTful servies.
  • Invest into readable and maintainable tests. If you let your tests rot you will sentence your service to death.
  • Don’t depend on the internals of the RESTful service under test (service classes, database schema). This makes your tests brittle.
  • Use POJOs and object mapping
    • Create tailored POJOs for your tests.
    • Always use POJOs to create request payload or to check the response payload. This makes your tests readable, concise and typesafe. Don’t fiddle with error-prone JSON strings.
    • Use fluent setters in your POJOs instead of cryptic endless argument lists. IntelliJ IDEA can generate fluent setters for you.
    • Check out Kotlin. The data classes are awesome for writing POJOs.
  • Use libraries
    • Rest-Assured to fluently create HTTP requests and assertions about the response.
    • AssertJ to create fluent, typesafe and readable assertions.
    • JsonPath (integrated into rest-assured) to easily navigate through JSON for simple checks (but generally prefer POJOs).
    • Awaitility to deal with asynchronous behavior.
    • Object mapper (like Jackson) to map between POJOs and JSON.
    • OkHttp MockWebServer to test REST clients.
  • Apply the clean code principles to your test code. Write tests that can be read like a story. For this, use sub-methods with a nice descriptive name. Keep your methods small.

Implementing Tests

Let’s code! Assume, we want to test a RESTful service offering information about blogs. The service provides the following resources that we want to test:

/blogs
/blogs/<blogId>

Use Rest-Assured

Rest-Assured provides a nice fluent API to create readable tests for REST resources.

import static com.jayway.restassured.RestAssured.given;

@Test
public void collectionResourceOK(){
     given()
             .param("limit", 20)
             .when()
             .get("blogs")
             .then()
             .statusCode(200);
}

Use Reusable RequestSpecifications

Create a RequestSpecification to reuse request configurations (base URL, parameters, content type, debugging logging) that you want to use for all requests.

private static RequestSpecification spec;

@BeforeClass
public static void initSpec(){
    spec = new RequestSpecBuilder()
            .setContentType(ContentType.JSON)
            .setBaseUri("http://localhost:8080/")
            .addFilter(new ResponseLoggingFilter())//log request and response for better debugging. You can also only log if a requests fails.
            .addFilter(new RequestLoggingFilter())
            .build();
}
@Test
public void useSpec(){
    given()
            .spec(spec)
            .param("limit", 20)
            .when()
            .get("blogs")
            .then()
            .statusCode(200);
}

Use POJOs and Object Mapping

Don’t fiddle with string concatenation or the cumbersome JsonObject to create JSON for a request or to check the responded JSON. This is verbose, not typesafe and error-prone.

Instead, create a separate POJO class and let an ObjectMapper like Jackson do the deserialization and serialization for you. Rest-assured provides built-in support for object mapping.

BlogDTO retrievedBlog = given()
        .spec(spec)
        .when()
        .get(locationHeader)
        .then()
        .statusCode(200)
        .extract().as(BlogDTO.class);
//check retrievedBlog object...

Use Fluent Setters for POJOs

Don’t use ordinary setters (verbose) or constructors with huge argument lists (hard to read and error-prone) to create test request payload. Instead, use fluent setters.

public class BlogDTO {

    private String name;
    private String description;
    private String url;

    //let your IDE generate the getters and fluent setters for your:

    public BlogDTO setName(String name) {
        this.name = name;
        return this;
    }

    public BlogDTO setDescription(String description) {
        this.description = description;
        return this;
    }

    public BlogDTO setUrl(String url) {
        this.url = url;
        return this;
    }

    // getter...
}

IntelliJ IDEA generates those setters for you. Moreover, there is a nice shortcut. Just write the fields (like private String name;) and hit Alt+Insert , Arrow-Down  until “Getter and Setter”, Enter , Select “Builder” for “Setter template”, then Shift+Arrow-Down  (multiple times) and finally press Enter. This is so cool!

Afterwards, you can use the fluent setters to write readable and typesafe code for creating test data.

BlogDTO newBlog = new BlogDTO()
        .setName("Example")
        .setDescription("Example")
        .setUrl("www.blogdomain.de");

Since object mapping is supported by rest-assured, we can just pass the object to rest-assured. Rest-assured will serialize the object and set the JSON in the request body.

String locationHeader = given()
        .spec(spec)
        .body(newBlog)
        .when()
        .post("blogs")
        .then()
        .statusCode(201)
        .extract().header("location");

Consider Kotlin instead of Java

The JVM language Kotlin allows defining POJOs with a single line of code. For instance, the class BlogDTO would look like this:

//definition:
data class BlogDTO (val name: String, val description: String, val url: String)

//usage:
val newBlog = BlogDTO(
        name = "Example",
        description = "Example",
        url = "www.blogdomain.de")

With Kotlin we can significantly reduce the boilerplate. The defined data class BlogDTO already contains a constructor, hashCode(), equals(), toString() and copy(). We don’t have to maintain them. Moreover, Kotlin supports named arguments. They make the constructor invocation very readable. This way, we don’t need fluent setters at all.

If you want to learn more about Kotlin check out my post “Kotlin. The Java Ecosystem deserves this Language”.

Please note, that you have to add the jackson-module-kotlin to your classpath in order to let the deserialization fly. Otherwise, Jackson will complain about the missing default constructor.

Use AssertJ to Check the Returned POJOs

AssertJ is an awesome library to write fluent, readable and typesafe test assertions. I like it much more than Hamcrest, because AssertJ guides you to find the fitting matcher for the current type.

You can use AssertJ to check the returned (and deserialized) POJO in the response body.

import static org.assertj.core.api.Assertions.assertThat;

BlogDTO retrievedBlog = given()
        .spec(spec)
        .when()
        .get(locationHeader)
        .then()
        .statusCode(200)
        .extract().as(BlogDTO.class);

assertThat(retrievedBlog.getName()).isEqualTo(newBlog.getName());
assertThat(retrievedBlog.getDescription()).isEqualTo(newBlog.getDescription());
assertThat(retrievedBlog.getUrl()).isEqualTo(newBlog.getUrl());

Use AssertJ’s isEqualToIgnoringGivenFields()

Usually, you want to check whether the retrieved bean is equal to the send bean that you wanted to create. Using a normal equals()-check doesn’t work because the service has generated an ID for the new entity. Hence, the beans differ in the ID fields. Fortunately, you can tell AssertJ to ignore certain fields during an equals-check. This way you don’t have to check every field manually.

assertThat(retrievedEntity).isEqualToIgnoringGivenFields(expectedEntity, "id");

Also check out the method isEqualToIgnoringNullFields().

Write Clean Test Code

Mind the clean code principles! Write test code that can be read as a story. Therefore, extract code to sub-methods with nice descriptive names. Keep your methods short. This will make your test readable and maintainable.

A rule of thumb: Every time you start building blocks within a method that is separated by an empty line and overridden by a comment, hold on! Instead, extract the code blocks to a new method and use the comment as a method name (Ctrl+Alt+M extracts the selected code to a new method in IntelliJ).

For instance, the following code can be understood very quickly:

@Test
public void createBlogAndCheckExistence(){
    BlogDTO newBlog = createDummyBlog();
    String blogResourceLocation = createResource("blogs", newBlog);
    BlogDTO retrievedBlog = getResource(blogResourceLocation, BlogDTO.class);
    assertEqualBlog(newBlog, retrievedBlog);
}

private BlogDTO createDummyBlog() {
    return new BlogDTO()
            .setName("Example Name")
            .setDescription("Example Description")
            .setUrl("www.blogdomain.de");
}

//nice reusable method
private String createResource(String path, Object bodyPayload) {
    return given()
            .spec(spec)
            .body(bodyPayload)
            .when()
            .post(path)
            .then()
            .statusCode(201)
            .extract().header("location");
}

//nice reusable method
private <T> T getResource(String locationHeader, Class<T> responseClass) {
    return given()
                .spec(spec)
                .when()
                .get(locationHeader)
                .then()
                .statusCode(200)
                .extract().as(responseClass);
}

private void assertEqualBlog(BlogDTO newBlog, BlogDTO retrievedBlog) {
    assertThat(retrievedBlog.getName()).isEqualTo(newBlog.getName());
    assertThat(retrievedBlog.getDescription()).isEqualTo(newBlog.getDescription());
    assertThat(retrievedBlog.getUrl()).isEqualTo(newBlog.getUrl());
}

If you stick to the Given-When-Then pattern for writing tests you’ll come to a similar result. Each part (given, when, then) should only contain a few lines (ideally one line). Try to extract each part in sub-methods to achieve this.

@Test
public void test(){
    //Given: set up the input for the action under test (test data, mocks, stubs)
    //When: execute the action you want to test.
    //Then: check the output with assertions
}

Create Reusable Methods for Common Operations on Resources

Take a look at the methods createResource()  and getResource()  above. These methods can be reused for every resource. You can put them in an abstract test class that can be extended by your concrete test classes.

Use AssertJ’s as()

Use AssertJ’s as() to add domain information to your assertion failure messages.

assertThat(retrievedBlog.getName()).as("Blog Name").isEqualTo(newBlog.getName());
assertThat(retrievedBlog.getDescription()).as("Blog Description").isEqualTo(newBlog.getDescription());
assertThat(retrievedBlog.getUrl()).as("Blog URL").isEqualTo(newBlog.getUrl());

Create Tailored POJOs

Only add the necessary fields to the POJO. The service returns a JSON with 20 properties but you are only interested in two of them? Add the @JsonIgnoreProperties(ignoreUnknown = true) annotation to your POJO class and Jackson won’t bother if there are more JSON properties in the response than fields in your POJO.

@JsonIgnoreProperties(ignoreUnknown = true)
public class BlogDTO {
    private String name;
    //the other JSON properties are not relevant for the test

    //...
}

Alternatively, you can set this configuration globally to the ObjectMapper:

objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

As you can see, using IDE’s code generation and @JsonIgnoreProperties makes writing POJOs for payload extremely easy and fast. There is no accuse for don’t using them.

Public Fields are not Always Forbidden

There are cases where you can simplify the POJO class code even more: Just make the fields public. OMG, did he really suggest this?!

@JsonIgnoreProperties(ignoreUnknown = true)
public class BlogListDTO {
    public int count;
    public List<BlogReference> blogs;
    public static class BlogReference{
        public int id;
        public String name;
        public String href;
    }
}

@Test
public void getBlogList(){
    BlogListDTO retrievedBlogs = given()
            .spec(spec)
            .when()
            .get("blogs")
            .then()
            .statusCode(200)
            .extract().as(BlogListDTO.class);
    assertThat(retrievedBlogs.count).isGreaterThan(7);
    assertThat(retrievedBlogs.blogs).isNotEmpty();
}

This can be useful when the response JSON contains nested data and you still want to use POJOs. Using public fields let you write nested POJO class with a few lines. But I only recommend this approach a) if the class is not reused in other projects (which should be true for test projects) and b) if the class is only used to map response payload. If you want to map request payload, you should go with fluent setters, because creating objects with public fields is clumsy. So be careful with this approach.

Use JsonPath for Simple Cases

If you are only interested in a single value of a JSON response, creating a POJO class for mapping is a little bit overkill. In this case JsonPath can be used to extract certain values out of a JSON document. JsonPath is like XPath for JSON.

Let’s say you want to retrieve a single value that is deeply nested.

JsonPath jsonPath = new JsonPath("{\"blogs\":[\"posts\":[{\"author\":{\"name\":\"Paul\"}}]]}");
String value = jsonPath.getString("blogs[0].posts[0].author.name");

There are two possible ways of using JsonPath with Rest-assured.

A: Converting the JSON to a JsonPath Object and use AssertJ to check it.

JsonPath retrievedBlogs = given()
        .spec(spec)
        .when()
        .get("blogs")
        .then()
        .statusCode(200)
        .extract().jsonPath();
assertThat(retrievedBlogs.getInt("count")).isGreaterThan(7);
assertThat(retrievedBlogs.getList("blogs")).isNotEmpty();

B: Rest-assured has built-in support for JsonPath, but then you have to use Hamcrest matchers.

import static org.hamcrest.Matchers.*;

given()
        .spec(spec)
        .when()
        .get("blogs")
        .then()
        .statusCode(200)
        .content("count", greaterThan(7))
        .content("blogs", is(not(empty())));

Although it’s more verbose, I personally prefer the AssertJ solution, because you don’t have to guess which Matcher works for which type.

However, with JsonPath you are tinkering with strings to address JSON properties, which is error-prone. For other cases, POJOs and object mapping are the better choices.

Dealing With Lists

When it comes to making assertions about lists, there are three ways of doing this.

A: Object mapping + AssertJ. Typesafe, readable, easy to debug, but more verbose.

BlogListDTO retrievedBlogList = given()
        .spec(spec)
        .when()
        .get("blogs")
        .then()
        .statusCode(200)
        .extract().as(BlogListDTO.class);
assertThat(retrievedBlogList.blogs)
        .extracting(blogEntry -> blogEntry.id)
        .contains(23);

The extracting() method is the killer feature of AssertJ. It allows to map from one item to another (like map() in the Java 8 Stream API). Apart from contains(), AssertJ offers a lot of other useful list methods like  containsAll(), containsExactly(), containsSequence() or doesNotContain().

B: JsonPath + Hamcrest. Concise, but more error-prone, harder to debug and you have to mind to use the right matcher for the type.

given()
        .spec(spec)
        .when()
        .get("blogs")
        .then()
        .statusCode(200)
        .content("blogs.id", hasItem(23));

But I concede that you can write really concise expressions with JsonPath. For instance “blogs.id” collects the id properties of each element in the blogs list into a new list.

C: JsonPath + AssertJ. 50% typesafe, quite verbose, but good to debug.

JsonPath retrievedBlogList = given()
        .spec(spec)
        .when()
        .get("blogs")
        .then()
        .statusCode(200)
        .extract().jsonPath();
assertThat(retrievedBlogList.getList("blogs.id"))
        .contains(23);

Dealing with Asynchronous Behavior (like Events)

Sometimes you have to wait in your test for an asynchronous event to take effect. You have to poll until a certain condition becomes true. Awaitility provides a nice API for waiting and polling until the assertion becomes true or a timeout exceeds.

import static com.jayway.awaitility.Awaitility.await;

sendAsyncEventThatCreatesABlog(123);
await().atMost(Duration.TWO_SECONDS).until(() -> {
    given()
            .when()
            .get("blogs/123")
            .then()
            .statusCode(200);
});

Note, that Awaitility’s methods return an immutable ConditionFactory.  This way you can configure the behavior for polling and waiting once and reuse it.

public static final ConditionFactory WAIT = await()
        .atMost(new Duration(15, TimeUnit.SECONDS))
        .pollInterval(Duration.ONE_SECOND)
        .pollDelay(Duration.ONE_SECOND);
@Test
public void waitAndPoll(){
    WAIT.until(() -> {
        //...
    });
}

Testing REST Clients with MockWebServer

If your service needs other services to fulfill its task, you may also want to test the class that does the REST request. This can be easily done with OkHttp MockWebServer and the best part is: you can run it as a unit test in the service project.

public class ImageReferenceServiceClientTest {

    private MockWebServer imageService;
    private ImageServiceClient imageClient;

    @Before
    public void init() throws IOException {
        imageService = new MockWebServer();
        imageService.start(); //uses an available port
        HttpUrl baseUrl = imageService.url("/images/");
        imageClient = new ImageServiceClient(baseUrl.host(), baseUrl.port());
    }

    @Test
    public void requestImage() throws JsonProcessingException {
        ImageReference expectedImageRef = new ImageReference().setId("123").setHref("http://images.company.org/123");
        String json = new ObjectMapper().writeValueAsString(expectedImageRef);
        imageService.enqueue(new MockResponse()
                .addHeader("Content-Type", "application/json")
                .setBody(json));

        ImageReference retrievedImageRef = imageClient.requestImage("123");

        assertThat(retrievedImageRef.getId()).isEqualTo(expectedImageRef.getId());
        assertThat(retrievedImageRef.getHref()).isEqualTo(expectedImageRef.getHref());
    }
}

You can also use the MockWebServer in an integration test in the test project.

If your client uses Spring’s RestTemplate, check out the MockRestServiceServer. However, I still prefer OkHttp’s MockWebServer because it spins up a real server on a dedicated port. The MockRestServiceServer intercepts into the RestTemplate code. So during the tests, your client behaves differently than in production. Besides, you can test network failures (timeouts, throttling, unreachable server) more easy and reliable. And finally, I personally don’t like Spring’s usage of many statically imported methods.

Decoupling and Dependencies

Never Rely on Internals of the REST Service

Tests for RESTful services are black box tests. Therefore, you should never rely on the internals of the RESTful service under test. This way your tests will stay robust. They don’t break in case of internal changes within the service. As long as the REST API doesn’t change your tests will work. This means:

  • Don’t have a dependency to the service project.
  • Don’t use any classes of the service project in your integration test project. Especially not the POJOs (domain model classes), although it’s sometimes tempting. If you need model classes, rewrite them. Using the right libraries and tools this is no big deal at all.
    • Moreover, you can tailor the classes to the requirements of your tests. You can use other languages (Kotlin), serialization frameworks (Jackson, Gson), field types or class nesting for your test POJOs.
    • Besides, using different POJOs ensures that you don’t break your API accidentally when you change an application POJO. In this case, your tests (with the API-conform POJOs) will fail and point you to this problem.
  • Try to avoid accessing the database of the service in order to create the test data. This leads to high coupling. If the schema or database technology changes, your tests will break. But sometimes it’s inevitable to access the database. Don’t be dogmatic. But be aware of the high coupling.

Besides, this black box approach reflects the reality: Your clients also don’t have implementation knowledge. So why should your tests have this? This helps to put yourself in the client’s place, which is in turn good to spot shortcomings.

Creating Test Data

When it comes to inserting the test data, you have several options:

  • Using the REST interface to insert the test data. You have to test the interface anyway, so why don’t you use the API which is supposed to be used for creating data? However, sometimes your contract doesn’t require resources for updating or inserting data.
  • Accessing the database directly. This is comfortable but leads to high coupling. Integration tests are black box tests and shouldn’t break if the internal database schema changes. But I concede that sometimes, database access inevitable.
  • Providing an additional resource for inserting data, which is only available during testing. You can use authentication or feature toggles to hide them in production. This way, your tests are more robust, because you don’t rely on the internals. However, you have to maintain additional resources. Besides, you have to take care to deactivate these testing resources, because they can significantly affect the security of your application or lead to misuse. So be careful.

Useful Tools and Tips

  • Postman is your best friend when it comes to ad-hoc testing of RESTful services.
  • If you prefer to create ad-hoc HTTP requests via the CLI, try HTTPie. It’s really nice and much better than cURL.
  • Adding JSONView to Chrome can also be useful sometimes.
  • During development I prefer to execute the tests out of IntelliJ IDEA instead of running Maven. But the tests usually need information about the service (URL, port, credentials) which are normally provided by Maven and passed to the tests via system properties during the build. But IntelliJ can also provide the necessary properties to the tests. For this you only have to change the Default (!) Run Configuration. Open “Edit Configuration…” and edit the Default Run Configuration for JUnit or TestNG and add the system properties (-Dhost=<host>, -Dport=<port>). From now on, these properties are provided to every test you execute within the IDE. Just click on the small green icon left to the test method and choose Run <Your Test>.
Add the necessary system properties to the Default Run Configuration for JUnit or TestNG

Add the necessary system properties to the Default Run Configuration for JUnit or TestNG

Afterwards you can start your parameterized integration test via the IDE.

Afterwards you can start your parameterized integration test via the IDE.

Test Project Structure

Where can we place our tests? We have two possibilities:

  • Place the integration tests within the service project (in the src/test/java folder) and execute them during the same build.
    • You have to distinguish between unit and integration tests (using naming conventions or annotations)
    • Easy setup. No need for a test environment, a separate Jenkins job and a build pipeline.
    • Longer build times, longer turnaround times and feedback cycles. You always have to execute the whole build including the tests.
    • May not work anyway, if your service needs other services to fulfill its tasks.
  • Create a separate integration test project for each service and execute them in a separate build/job.
    • You can easily run only the integration tests without building the whole application. This shortens the turnaround times.
    • You have to set up a test stage and a build pipeline: Build the service, set up a test environment, deploy the service and run the integration tests against it.
    • Better decoupling between the service project and the integration tests.
    • It’s easier for other teams (using your service) to contribute their assumption about the service behavior in form of tests to the test project.

Bottom line: If your service is small, there are not many integration tests and you don’t need other services during your tests. You can include the integration tests into your normal build life cycle. However, when your system of services becomes complex and interrelated, you need to set up a dedicated test environment anyway. In this case, it’s better to locate the tests for the service in a separate project and run them against the deployed service in the test environment.

Source Code and Examples

I created a small Github project testingrestservice to show the test code in action based on a small Spring Boot service.