Creating RESTful APIs with Spring Boot is a straightforward process, making it a popular choice for a variety of applications, from UI to batch processing. The same API created can be used anywhere, whether it’s called from a UI application or batch applications.
However, testing these APIs to ensure they work correctly can be challenging. In this article, I’ll introduce you to Rest-Assured, a powerful and easy-to-use Java-based library that simplifies the testing of RESTful APIs. It has a lot of features for validating and verifying the response of HTTP requests. We’ll walk through the steps to set up and use Rest-Assured by building a simple API for a book club and demonstrating how to write effective unit tests for it.
Setting Up Our Demo Book Club API
To introduce you to Rest-Assured, I’m going to build a quick, simple API based on the premise of a book club. To begin with, this API needs to be able to provide a list of all of the books available, retrieve a book by its unique ID, and search books based on the author’s last name, title, and genre. This API is backed by a Spring Boot service, which in turn uses Spring Data to query the book club database.
For this demonstration, though, the service is returning mocked data, and the data retrieval implementation won’t be represented here. In a real-world application, we would use an in-memory database for testing that defined the table structures and seed data that the API and service needed.
Here’s a simple example RestController
that we’ll be testing with Rest-Assured:
@RestController @RequestMapping(path="/api/v1/bookClub") public class ApiDemoController { @Autowired private BookService bookService; @GetMapping() public List<Book> getAllBooks() { return bookService.findAllBooks(); } @GetMapping("/{id}") public Book getBookById(@PathVariable Integer id) { return bookService.findById(id); } @PostMapping(path = "/search", consumes= {MediaType.APPLICATION_JSON_VALUE}) public List<Book> searchBooks( @RequestBody BookSearchRequest searchRequest) { return bookService.searchBooks(searchRequest); } }
Using the @RestController
annotation on the class tells Spring Framework that we are defining a controller where any request mappings assume @ResponseBody
semantics by default. This means that any response from a request mapping automatically converts a POJO to JSON, simplifying the code.
Using @RequestMapping
at the class level and specifying the path variable sets the base path for all of the method-level request mappings.
Next, we have autowired our BookService
into the controller, which, in this example, just serves up mocked data for the example.
Lastly, we have a few methods that define some API endpoints, each of which is slightly different to allow a basic test intro. @GetMapping
without any specific path and this will be our default mapping that defines an HTTP Get request at the base URL. This method returns a list of Book objects from the BookService.
Here’s the POJO that represents a Book:
public class Book { private String authorFirstName; private String authorLastName; private int bookId; private List<String> genres = new ArrayList<>(); private String publicationDate; private String title; public String getAuthorFirstName() { return authorFirstName; } public String getAuthorLastName() { return authorLastName; } public int getBookId() { return bookId; } public List<String> getGenres() { return genres; } public String getPublicationDate() { return publicationDate; } public String getTitle() { return title; } public void setAuthorFirstName(String authorFirstName) { this.authorFirstName = authorFirstName; } public void setAuthorLastName(String authorLastName) { this.authorLastName = authorLastName; } public void setBookId(int bookId) { this.bookId = bookId; } public void setGenres(List<String> genres) { this.genres = genres; } public void setPublicationDate(String publicationDate) { this.publicationDate = publicationDate; } public void setTitle(String title) { this.title = title; } }
Creating Initial JUnit Tests for the API
Now that we’ve defined a very basic API that returns a list of all Books available to the book club members, let’s take a look at creating the initial JUnit test for the API. We’ll do this iteratively as well, starting with just a shell of a test and then adding in the test methods as we go along in building the API.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ApiDemoApplicationTests { private static final String BASE_URL = "/api/v1/bookClub"; @LocalServerPort private int port; @BeforeEach public void init() { RestAssured.baseURI = "http://localhost"; RestAssured.port = this.port; } }
What we’ve created here is a common shell of a unit test for Spring Boot, using the @SpringBootTest
annotation and declaring the web environment with a random port to use. You can obtain the port number that is in use during the test execution with the @LocalServerPort
annotation, as indicated in the code example. In the init
method we have defined the base URI and port that will be needed to call the various API endpoints using Rest-Assured.
The first test I want to demonstrate is for the endpoint that returns a book by its ID. For this particular test, I want to show how to make the request call to the endpoint, then receive and interrogate the response that is returned on a field-by-field basis.
Here’s the code for that test:
@Test void shouldReturnBookById() throws JSONException { RestAssured .given().log().uri().log().method() .when().get(BASE_URL + "/3") .then().log().status().log().body().statusCode(200) .body("title", equalTo("Life, the Universe and Everything")) .body("authorFirstName", equalTo("Adams")) .body("authorLastName", equalTo("Douglas")); }
Rest Assured tests have methods that allow you to follow the given/when/then approach to a test. For this particular case, we’ve also incorporated some methods that allow you to log the different elements of the request and response. Let’s break this down into its individual parts.
.given().log().uri().log().method()
- Given the URI and method.
- Log the URI called for the request
- Log the Method used for the request
when().get(BASE_URL + "/3")
- When the endpoint is called with the value of 3 as the path parameter for ID
.then().log().status().log().body().statusCode(200)
- Log the status code returned
- Log the response body returned.
- Assert that the status code return should be 200
.body("title", equalTo("Life, the Universe and Everything"))
.body("authorFirstName", equalTo("Adams"))
.body("authorLastName", equalTo("Douglas"));
- These are additional assertions for specific fields returned in the body of the JSON response.
Logging the request and response details can prove to be useful within an automated build environment, especially when troubleshooting test failures. Here’s an example of what the logging output will look like:
Request URI: http://localhost:63126/api/v1/bookClub/3 Request method: GET HTTP/1.1 200 { "authorFirstName": "Adams", "authorLastName": "Douglas", "bookId": 3, "genres": [ "science fiction", "fiction" ], "publicationDate": "1975-10-17", "title": "Life, the Universe and Everything" }
Test Using JSONAssert
Now that I’ve shown you how to individually test the individual fields of a JSON response, here’s an alternative solution that I think is a bit simpler and with less code. This test will be for the default @GetMapping
that returns all of the books available within the Book Club. Instead of interrogating the individual fields within the JSON response body, we’re going to introduce the usage of JSONAssert. JSONAssert is a library that contains a set of useful methods that make comparing JSON data simple, even if the data is complex.
Here’s the code for this test:
@Test void shouldReturnAllData() throws JSONException { String responseBody = RestAssured .given().log().uri().log().method() .when().get(BASE_URL) .then().log().status().log().body().statusCode(200) .extract().body().asString(); final String expectedResponseBody = """ [ { "bookId": 0, "title": "The Shining", "authorFirstName": "Stephen", "authorLastName": "King", "publicationDate": "1977-01-08", "genres": [ "horror", "fiction" ] }, { "bookId": 1, "title": "Dreamcatcher", "authorFirstName": "Stephen", "authorLastName": "King", "publicationDate": "2001-02-20", "genres": [ "horror", "fiction" ] }, { "bookId": 2, "title": "The Hitchhiker's Guide to the Galaxy", "authorFirstName": "Adams", "authorLastName": "Douglas", "publicationDate": "1978-10-03", "genres": [ "science fiction", "fiction" ] }, { "bookId": 3, "title": "Life, the Universe and Everything", "authorFirstName": "Adams", "authorLastName": "Douglas", "publicationDate": "1975-10-17", "genres": [ "science fiction", "fiction" ] } ] """; JSONAssert.assertEquals(expectedResponseBody, responseBody, true); }
As you can see, the basic premise of the given/when/then scenario is still present. The difference is, that instead of interrogating and validating the JSON response body at a field-by-field level is that this test is instead validating the whole JSON response body returned from the API against an expected block of JSON data. Here, we’ve defined the expected JSON data using Java’s new text block feature for defining a string value.
This works well for a simple data structure that these examples are using. If you have a more complex and large data structure, you might consider creating JSON data files for the expected responses and reading those into the test for comparisons. The Boolean flag as the last method parameter indicates whether or not to do a strict comparison of the data structure. JSONAssert will make a logical comparison of the data structure. For more information about JSONAssert, visit https://github.com/skyscreamer/JSONassert.
The last test I wanted to demonstrate is for the endpoint that accepts a JSON request body using the POST method. Instead of using a path variable or query parameter using the GET
method, this endpoint consumes JSON data and converts it to the BookSearchRequest
object. Here’s the code for the BookSearchRequest
object:
public record BookSearchRequest( String authorLastName, String genre, String title) { }
Here’s the code listing for the PostMapping
method in the controller. Note that by use of the @RestController
annotation on the class, the @RequestBody
annotation prefixing the BookSearchRequest
method parameter, and specifying that the mapping consumes JSON data Spring will automatically map the JSON data to the BookSearchRequest
object.
@PostMapping(path = "/search", consumes= {MediaType.APPLICATION_JSON_VALUE}) public List<Book> searchBooks( @RequestBody BookSearchRequest searchRequest) { return bookService.searchBooks(searchRequest); }
Here’s the code for the test. First, we define the request body that we’ll POST to the endpoint. Then, similar to the other tests, we will call the API endpoint using the POST method and supply the request body, verifying that the status code returned is 200. Then, we will extract the response body JSON data and make our assertion based on what we expect the response to be.
@Test void shouldReturnSearchedBookData() throws JSONException { String requestBody = """ { "authorLastName" : "King" }"""; String responseBody = RestAssured .given().log().uri().log().method() .contentType("application/json") .body(requestBody) .when().post(BASE_URL + "/search") .then().log().status().log().body().statusCode(200) .extract().body().asString(); String expectedResponseBody = """ [ { "bookId": 0, "title": "The Shining", "authorFirstName": "Stephen", "authorLastName": "King", "publicationDate": "1977-01-08", "genres": [ "horror", "fiction" ] }, { "bookId": 1, "title": "Dreamcatcher", "authorFirstName": "Stephen", "authorLastName": "King", "publicationDate": "2001-02-20", "genres": [ "horror", "fiction" ] } ] """; JSONAssert.assertEquals(expectedResponseBody, responseBody, true); }
Again, we’re still following the given/when/then approach to the testing structure. The biggest difference here is that we’re using the POST method and supplying a JSON request body when the endpoint is called. We’re still expecting a specific response back based on the search we are performing.
This is obviously a simple test where we aren’t using a database and are mocking the data on the basis of providing a simple demo. In our “real world” environment, we would likely be using an in-memory database in which the schema mimics the actual production database. However, the in-memory database would contain seed data that would allow you to test various scenarios that would exercise all of the code, thus providing an adequate amount of code coverage. Using the highly controlled in-memory database also provides a stable set of data that only changes as required by any feature changes in the code.
Demo Wrap Up
Through this demo, we’ve explored the basics of setting up and using Rest-Assured for testing a Spring Boot-based API. We’ve seen how Rest-Assured offers a robust and flexible solution for testing RESTful APIs with seamless integration with Spring. As I stated, these have been pretty simple straightforward tests in the demo, but here are some of the additional features that Rest Assured supports:
- JSON Schema validation
- XML Data and XML Schema validation
- Specifying request data that use cookies, headers, multi-value parameters
- Verifying response data that use cookies and headers and testing the response time.
- Authentication: Basic, Digest, Form, OAuth, CSRF
- Filters
- SSL
- Proxy Configuration
- Spring support
If you have recently started implementing RESTful APIs or have been developing them for a while now, hopefully, this will give you some ideas that will simplify your testing needs and provide the code coverage you need. Although all of these examples were written for a Spring Boot Controller and its API endpoints, Rest Assured will work against any API. The usage of Rest Assured is simply calling a provided URL using a RESTful method and evaluating the response for validation.
Whether you’re new to API development or looking to enhance your testing strategy, Rest-Assured can help you achieve comprehensive test coverage with minimal effort. Give it a try, and see how it can simplify your API testing process.