The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:
Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
1. Introduction
REST-assured was designed to simplify the testing and validation of REST APIs and is highly influenced by testing techniques used in dynamic languages such as Ruby and Groovy.
The library has solid support for HTTP, starting of course with the verbs and standard HTTP operations, but also going well beyond these basics.
In this guide, we are going to explore REST-assured and weβre going to use Hamcrest to do assertion. If youβre not already familiar with Hamcrest, you should first brush up with the tutorial: Testing with Hamcrest.
Also, to learn about more advanced use cases of REST-assured, check out our other articles:
- REST-assured with Groovy
- JSON Schema Validation with REST-assured
- Parameters, Headers and Cookies with REST-assured
Now letβs dive in with a simple example.
2. Simple Example Test
Before we get started, letβs ensure that our tests have the following static imports:
io.restassured.RestAssured.*
io.restassured.matcher.RestAssuredMatchers.*
org.hamcrest.Matchers.*
Weβll need that to keep tests simple and have easy access to the main APIs.
Now, letβs get started with a simple example β a basic betting system exposing some data for games:
{
"id": "390",
"data": {
"leagueId": 35,
"homeTeam": "Norway",
"visitingTeam": "England",
},
"odds": [{
"price": "1.30",
"name": "1"
},
{
"price": "5.25",
"name": "X"
}]
}
Letβs say that this is the JSON response from hitting http://localhost:8080/events?id=390 β the locally deployed API.
Letβs now use REST-assured to verify some interesting features of the response JSON:
@Test
public void givenUrl_whenSuccessOnGetsResponseAndJsonHasRequiredKV_thenCorrect() {
get("/events?id=390").then().statusCode(200).assertThat()
.body("data.leagueId", equalTo(35));
}
So, what we did here is verify that a call to the endpoint /events?id=390 responds with a body containing a JSON String whose leagueId of the data object is 35.
Letβs have a look at a more interesting example. Letβs say we would like to verify that the odds array has records with prices 1.30 and 5.25:
@Test
public void givenUrl_whenJsonResponseHasArrayWithGivenValuesUnderKey_thenCorrect() {
get("/events?id=390").then().assertThat()
.body("odds.price", hasItems("1.30", "5.25"));
}
3. REST-assured Setup
If weβre using Maven, we add the rest-assured dependency in the pom.xml file:
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
REST-assured takes advantage of the power of Hamcrest matchers to perform its assertions, so we must include the hamcrest dependency as well:
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.1</version>
</dependency>
4. Anonymous JSON Root Validation
Consider an array that comprises primitives rather than objects:
[1, 2, 3]
This is called an anonymous JSON root, meaning that it has no key-value pair, nevertheless, it is still valid JSON data.
We can run validation in such a scenario by using the $ symbol or an empty String (ββ) as the path. Assume we expose the above service through http://localhost:8080/json β then we can validate it with REST-assured:
when().get("/json").then().body("$", hasItems(1, 2, 3));
This approach does the same:
when().get("/json").then().body("", hasItems(1, 2, 3));
5. Floats and Doubles
When we start using REST-assured to test our REST services, we need to understand that floating point numbers in JSON responses are mapped to primitive type float.
The use of float type is not interchangeable with double:
{
"odd": {
"price": "1.30",
"ck": 12.2,
"name": "1"
}
}
If we run the following test for the value of ck:
get("/odd").then().assertThat().body("odd.ck", equalTo(12.2));
This test will fail even if the value we are testing is equal to the value in the response. This is because weβre comparing to a double rather than to a float.
To make it work, we have to explicitly specify the operand to the equalTo matcher method as a float:
get("/odd").then().assertThat().body("odd.ck", equalTo(12.2f));
6. Specifying the Request Method
Typically, we would perform a request by calling a method such as get(), corresponding to the request method we want to use.
In addition, we can also specify the HTTP verb using the request() method:
@Test
public void whenRequestGet_thenOK(){
when().request("GET", "/users/eugenp").then().statusCode(200);
}
The example above is equivalent to using get() directly.
Similarly, we can send HEAD, CONNECT, and OPTIONS requests:
@Test
public void whenRequestHead_thenOK() {
when().request("HEAD", "/users/eugenp").then().statusCode(200);
}
POST requests also follow a similar syntax, and we can specify the body by using the with() and body() methods.
Therefore, to create a new Odd by sending a POST request, we can run:
@Test
public void whenRequestedPost_thenCreated() {
with().body(new Odd(5.25f, 1, 13.1f, "X"))
.when()
.request("POST", "/odds/new")
.then()
.statusCode(201);
}
The Odd object sent as body will automatically be converted to JSON. We can also pass any String that we want to send as our POST body.
7. Default Values Configuration
We can configure a lot of default values for the tests:
@BeforeEach
public void setup() {
RestAssured.baseURI = "https://api.github.com";
RestAssured.port = 443;
}
Here, weβre setting a base URI and port for our requests. Besides these, we can also configure the base path, root path, and authentication.
Note that we can also reset to the standard REST-assured defaults by using:
RestAssured.reset();
8. Measure Response Time
Letβs see how we can measure the response time using the time() and timeIn() methods of the Response object:
@Test
public void whenMeasureResponseTime_thenOK() {
Response response = RestAssured.get("/users/eugenp");
long timeInMS = response.time();
long timeInS = response.timeIn(TimeUnit.SECONDS);
assertEquals(timeInS, timeInMS/1000);
}
Note that, in the above example:
- time() is used to get response time in milliseconds
- timeIn() is used to get response time in the specified time unit
8.1. Validate Response Time
We can also validate the response time β in milliseconds β with the help of a simple long Matcher:
@Test
public void whenValidateResponseTime_thenSuccess() {
when().get("/users/eugenp").then().time(lessThan(5000L));
}
If we want to validate the response time in a different time unit, then weβll use the time() matcher with a second TimeUnit parameter:
@Test
public void whenValidateResponseTimeInSeconds_thenSuccess(){
when().get("/users/eugenp").then().time(lessThan(5L),TimeUnit.SECONDS);
}
9. XML Response Verification
Not only can it validate a JSON response, it can validate XML as well.
Letβs assume we make a request to http://localhost:8080/employees and we get the XML response:
<employees>
<employee category="skilled">
<first-name>Jane</first-name>
<last-name>Daisy</last-name>
<sex>f</sex>
</employee>
</employees>
Given this response, letβs verify that the first-name is Jane:
@Test
public void givenUrl_whenXmlResponseValueTestsEqual_thenCorrect() {
post("/employees").then().assertThat()
.body("employees.employee.first-name", equalTo("Jane"));
}
We can also verify that all values match our expected values by chaining body matchers together:
@Test
public void givenUrl_whenMultipleXmlValuesTestEqual_thenCorrect() {
post("/employees").then().assertThat()
.body("employees.employee.first-name", equalTo("Jane"))
.body("employees.employee.last-name", equalTo("Daisy"))
.body("employees.employee.sex", equalTo("f"));
}
Or we can use the shorthand version with variable arguments:
@Test
public void givenUrl_whenMultipleXmlValuesTestEqualInShortHand_thenCorrect() {
post("/employees")
.then().assertThat().body("employees.employee.first-name",
equalTo("Jane"),"employees.employee.last-name",
equalTo("Daisy"), "employees.employee.sex",
equalTo("f"));
}
10. XPath for XML
We can also verify our responses using XPath. Consider an example that executes a matcher on the first-name:
@Test
public void givenUrl_whenValidatesXmlUsingXpath_thenCorrect() {
post("/employees").then().assertThat().
body(hasXPath("/employees/employee/first-name", containsString("Ja")));
}
XPath also accepts an alternate way of running the equalTo matcher:
@Test
public void givenUrl_whenValidatesXmlUsingXpath2_thenCorrect() {
post("/employees").then().assertThat()
.body(hasXPath("/employees/employee/first-name[text()='Jane']"));
}
11. Logging Test Details
11.1. Log Request Details
First, letβs see how to log entire request details using log().all():
@Test
public void whenLogRequest_thenOK() {
given().log().all()
.when().get("/users/eugenp")
.then().statusCode(200);
}
This will log something like:
Request method: GET
Request URI: https://api.github.com:443/users/eugenp
Proxy: <none>
Request params: <none>
Query params: <none>
Form params: <none>
Path params: <none>
Multiparts: <none>
Headers: Accept=*/*
Cookies: <none>
Body: <none>
To log only specific parts of the request, instead of all(), we can call params(), body(), headers(), cookies(), method(), path():
@Test
public void whenLogRequest_thenOK() {
given().log().params()
.when().get("/users/eugenp")
.then().statusCode(200);
}
Note that other libraries or filters used may alter whatβs actually sent to the server, so this should only be used to log the initial request specification.
11.2. Log Response Details
Similarly, we can log the response details.
In the following example, weβre logging the response body only:
@Test
public void whenLogResponse_thenOK() {
when().get("/repos/eugenp/tutorials")
.then().log().body().statusCode(200);
}
Sample output:
{
"id": 9754983,
"name": "tutorials",
"full_name": "eugenp/tutorials",
"private": false,
"html_url": "https://github.com/eugenp/tutorials",
"description": "The \"REST With Spring\" Course: ",
"fork": false,
"size": 72371,
"license": {
"key": "mit",
"name": "MIT License",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit"
},
...
}
11.3. Log Response if Condition Occurs
We also have the option of logging the response only if an error occurs or the status code matches a given value:
@Test
public void whenLogResponseIfErrorOccurred_thenSuccess() {
when().get("/users/eugenp")
.then().log().ifError();
when().get("/users/eugenp")
.then().log().ifStatusCodeIsEqualTo(500);
when().get("/users/eugenp")
.then().log().ifStatusCodeMatches(greaterThan(200));
}
11.4. Log if Validation Fails
We can also log both request and response only if our validation fails:
@Test
public void whenLogOnlyIfValidationFailed_thenSuccess() {
when().get("/users/eugenp")
.then().log().ifValidationFails().statusCode(200);
given().log().ifValidationFails()
.when().get("/users/eugenp")
.then().statusCode(200);
}
In this example, we want to validate that the status code is 200. Only if this fails, the request and response will be logged.
12. Conclusion
In this tutorial, weβve explored the REST-assured framework and looked at its most important features that we can use to test our RESTful services and validate their responses.
