API Testing with Java and Spring Boot Test - Part 2: Improving the solution

API Testing with Java and Spring Boot Test - Part 2: Improving the solution

Luiz Martins · February 6, 2023

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 called FileUtils.

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);
    }
}

Show code in Github Gist

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"
}

Show code in Github Gist

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)));
    }
}

Show code in Github Gist

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>

Show code in Github Gist

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);
    }
}

Show code in Github Gist

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);
    }
}

Show code in Github Gist

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).


Luiz Martins