
API Testing with Java and Spring Boot Test - Part 2: Improving the solution
In the last part of this step-by-step, we created the project, set up the test framework, and also did all the configurations needed to run our API tests.
You can see the first part of the series here:
Let's continue to grow our test framework, but first, we need to do some improvements to the existing code. In this guide, we'll:
- Refactor the object mapping (to be easier to handle with the JSON files)
- Improve the response validations
- Handle multiple environments inside our tests.
These changes will make our code base cleaner and easier to maintain for us to create a scalable framework of API tests.
Let's do it.
Refactoring the Object mapping
We'll take advantage of using the Spring boot Repository
to separate the responsibility of mapping the objects (JSON) we're going to use inside our tests. That way, we can do another step forward in our code cleanup.
So, first of all, we're going to:
- Create a new package called
repositories
- Then we create a new
Class
inside this package calledFileUtils
.
We'll also take the opportunity to change the way we map the object to not be hard-coded but be in a proper resource file. That way when we need to change the test data, we don't have to change the test but only the correspondent resource file.
package org.example.repositories;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Repository;
import java.io.IOException;
import java.net.URL;
@Repository
public class FileUtils {
/**
* Read File and return JsonNode
*
* @param filePath
* @return
* @throws IOException
*/
public static JsonNode readJsonFromFile(String filePath) throws IOException {
ObjectMapper mapper = new ObjectMapper();
URL res = FileUtils.class.getClassLoader().getResource(filePath);
if (res == null) {
throw new IllegalArgumentException(String.format("File not found! - %s", filePath));
}
return mapper.readTree(res);
}
}
As you can see in the file above, we created a function to read a JSON file and then return the object already mapped - similar to the approach we had before in the test file.
Now, we'll structure the resources
folder to accommodate the JSON files.
In the resources
folder, let's create a new directory called user
and then create a file to store the request body of the operation we'll do.
{
"name": "Luiz Eduardo",
"job": "Senior QA Engineer"
}
After that, we need to update our test. Now we want to get the file data by using the new function we created for that purpose. The updated test will look like that:
package api.test.java.tests;
import com.fasterxml.jackson.databind.JsonNode;
import io.restassured.response.Response;
import org.example.repositories.FileUtils;
import org.example.services.YourApiService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.io.IOException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ApiTest {
private final YourApiService yourApiService;
public ApiTest(YourApiService yourApiService) {
this.yourApiService = yourApiService;
}
@Test
public void testCreateUser() throws IOException {
JsonNode requestBody = FileUtils.readJsonFromFile("user/createUser.json");
Response res = yourApiService.postRequest("/users", requestBody);
assertThat(res.statusCode(), is(equalTo(201)));
}
}
Much better! By keeping the code cleaner we are helping our future selves with its maintenance - trust me, you'll be very glad to see this.
Improving the response validation
Great! Now, let's have a look at the response validation.
In some cases, we want to check the full response body - or at least some parts of it - to fulfill the test requirements.
To do that, we'll create:
- A new
Repository
to abstract the responsibility and help us check the full JSON response body - A function to handle the check of the JSON response.
We'll also add the "jsonassert" dependency to assert the JSON.
The pom.xml
file will look like that:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>api.test.java</groupId>
<artifactId>apitest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>api-test-java</name>
<description>Api Tests</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.3.0</version>
<exclusions><!-- https://www.baeldung.com/maven-version-collision -->
<exclusion>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-xml</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.5.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The newly created ResponseUtils
class will be something like this:
package org.example.repositories;
import com.fasterxml.jackson.databind.JsonNode;
import io.restassured.response.Response;
import org.json.JSONException;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.stereotype.Repository;
@Repository
public class ResponseUtils {
public static void assertJson(String actualJson, String expectedJson, JSONCompareMode mode) throws JSONException {
JSONAssert.assertEquals(expectedJson, actualJson, mode);
}
public static void assertJson(Response response, JsonNode expectedJson) throws JSONException {
assertJson(response.getBody().asString(), expectedJson.toString(), JSONCompareMode.LENIENT);
}
}
The next step should be to use this new function and improve our test. To do that, we'll configure a GET request on YourApiService
and return the full Response
object. Then we should be able to check the response body.
public Response getRequest(String endpoint) {
return RestAssured.given(spec)
.contentType(ContentType.JSON)
.when()
.get(endpoint);
}
Now, it's just a matter of adding the test case to the ApiTest
test class and using the same strategy of letting the JSON response file be in its proper directory. Finally, we'll have something like this:
@Test
public void testGetUser() throws IOException, JSONException {
Response res = yourApiService.getRequest("/users/2");
JsonNode expectedResponse = FileUtils.readJsonFromFile("responses/user/specific.json");
assertThat(res.statusCode(), is(equalTo(200)));
ResponseUtils.assertJson(res, expectedResponse);
}
Quite easy to understand if you just look at the test case :)
Executing the tests over multiple environments
Now we have the tests properly set, and everything is in the right place. One thing that could be in your mind right now is: "Ok, but I have a scenario in my product, in which I need to run my test suit over multiple environments. How do I do that?".
And the answer is - property files.
The property files are used to store specific data which we can use along our test suit, like the application host, port, and path to the API. You can also store environment variables to use within your test framework. However, be careful, since we don't want to make this information public. You can see an example in the lines below.
With Spring boot, we take advantage of using the "profiles" to set the specifics of the environments our application has, and make them available as spring boot profiles.
So, let's do that. Inside the resources
folder, we'll create a new file called application-prod.properties
to store the values of the production cluster of the test application. The file will store something like this:
apitest.base.uri=https://reqres.in
apitest.base.path=/api
apitest.token=${TOKEN}
Now, the only thing missing is to change our service to get the values stored in the property file.
To get the values from the property files, we'll use the annotation @Value
. This annotation will provide the values from the properties we set in the application-prod.properties
file.
**Bear in mind: ** You'll need to set the environment variable before using it here. The @Value
annotation will grab this value from the environment variables you have set.
The updated version of YourApiService
class will look like this:
package org.example.services;
import com.fasterxml.jackson.databind.JsonNode;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
@Slf4j
@Service
public class YourApiService {
@Value("${apitest.base.uri}")
private String baseURI;
@Value("${apitest.base.path}")
private String basePath;
@Value("${apitest.token}")
private String myToken;
private RequestSpecification spec;
@PostConstruct
protected void init() {
RestAssured.useRelaxedHTTPSValidation();
spec = new RequestSpecBuilder().setBaseUri(baseURI).setBasePath(basePath).build();
}
public Response postRequest(String endpoint, JsonNode requestBody) {
return RestAssured.given(spec)
.contentType(ContentType.JSON)
.body(requestBody)
.when()
.post(endpoint);
}
public Response getRequest(String endpoint) {
return RestAssured.given(spec)
// In our case, we won't use the "token" variable, as the API doesn't require so.
// But if your API require, here you can use the token like this:
// .auth().basic("token", myToken)
.contentType(ContentType.JSON)
.when()
.get(endpoint);
}
}
That's a great step up. This way, if you have multiple environments in your setup, you just need to create another application-YOUR_PROFILE_NAME.properties
.
Executing the test suit
You must be wondering: How do I run the test suit with this newly created profile?
The answer is simple, just execute mvn clean test -Dspring.profiles.active=prod
.
By default, if you just run the mvn clean test
command, Spring Boot will try to find a file called application.properties
and automatically activate it.
Now we have significantly improved the test setup of our application by:
- The refactoring of the Object mapping to clean up our code and apply some best practices
- Improving the response validation by adding a new dependency and using it to simplify the check
- Learning how to handle multiple test environments. This should be useful when it comes to companies that have layers of environments before the code reach production
Are you curious about the article? Building a Java API test framework part 3 will further improve our application. We will then go deeper into the following topics:
- Test reporting with Allure reports
- Configure a CI pipeline with GitHub actions
- Publish the test report on GitHub pages
(Image by Mohammad Rahmani on Unsplash).
Related articles

Hesam Sameni
My Journey at Mercedes-Benz.io - Unity and Multicultural Friendship
I was born in Iran back in 1990, and while I studied architecture at university, my real passion was always technology. I loved all things tech since I was a kid. So, even though I started as an interior designer, I eventually quit that job to teach myself web development.
Sep 20, 2023

Thanh Hoang Nguyen
Changing Careers | Catarina Marques Journey
At a certain point in our careers, many of us feel stuck, lost, and lacking purpose. Taking the leap to make a change can be overwhelming, whether it involves switching roles within a company or embarking on an entirely new career path. In this article, we introduce Catarina Marques, a MB.ioneer who bet all her cards to become a Product designer.
Jul 26, 2023

Rosa Acri
#ProductCon - Product Management Conference
This article was originally published by Rosa Acri on LinkedIn.
Jul 11, 2023