Performing end-to-end testing for both API and UI components offers several advantages, including comprehensive validation of the entire system, identification of integration issues between frontend and backend, assurance of data consistency, validation of business workflows, improved test coverage by combining UI and API tests, early detection of issues in the development lifecycle, enhanced automation possibilities, better collaboration between development teams, and user-centric validation to ensure a positive user experience. This holistic testing approach contributes to the overall reliability, functionality, and performance of the application, addressing issues across the entire stack and supporting continuous integration and delivery practices in modern software development.
This research blog delves into the intricacies of API response validation, exploring two distinct yet impactful approaches. By shedding light on practical scenarios and employing a comparative analysis, we aim to empower developers with insights that transcend conventional wisdom.
The ScenarioÂ
Let's bring our exploration down to a practical scenario: imagine an API response packed with information. Our attention is specifically on verifying a particular element — a list of alert codes for various cities and matches with the expected codes. This everyday yet complex situation sets the stage for our comparative analysis.
Sample API Response:
{
  "data": {
    "country": {
      "state": [
        {
          "city": [
            {"alert": "Abc", "status": "A"},
            {"alert": "Bcd", "status": "A"},
{"alert": "Cde", "status": "I"},
            {"alert": "Def", "status": "I"},
            // ... more city objects
          ]
        },
        // ... more state objects
      ]
    }
  }
}
The requirement is to iterate through each JSON object, validate the alert code, and cross-reference it with the expected alert code.The API response should include only the codes that match my expected alert codes.
Expected Alert Codes:
List<String> expectedAlertCodes = Arrays.asList("Abc", "Bcd", "Cde", "Def");
A Research-Driven Exploration
Embarking on a research-driven journey, this blog illuminates two distinct paradigms for handling matches in API response validation.Â
As we dissect each approach, we not only decipher the intricacies of the code but also unravel the inherent benefits that guide the choice between these methodologies.
Approach 1: Traditional Iteration
Exploring traditional methods, we adopt a coding approach that recalls classic methodologies. The loop structure, known to experienced developers, is simple and practical. With clear iterations and early exits, this method is effective due to its straightforwardness and practicality.
//All imports here…….
public class ApiResponseValidation {
    public static void main(String[] args) {
        // API endpoint
        String apiEndpoint = "api_endpoint_here";
        // Perform the API request and get the response
        String response = RestAssured.given()
                .contentType(ContentType.JSON)
                .when()
                .get(apiEndpoint)
                .then()
                .extract().response().asString();
        // Our Expected alert codes
        List<String> expectedAlertCodes = Arrays.asList("Abc", "Bcd", "Cde", "Def");
        // Perform validation on the API response(Method calling)
        validateApiResponse(response, expectedAlertCodes);
    }
    private static void validateApiResponse(String response, List<String> expectedAlertCodes) {
        // Parse the response and extract alert codes
        List<String> actualAlertCodes = RestAssured.given().body(response)
                .jsonPath().getList("data.country.state.city.alert");
        // Compare actual and expected alert codes
        for (String expectedCode : expectedAlertCodes) {
            if (!actualAlertCodes.contains(expectedCode)) {
                System.out.println("Validation failed: Alert code '" + expectedCode + "' not found in the response.");
            }
        }
        // Print success message if all expected codes are found
        System.out.println("Validation successful: All expected alert codes found in the response.");
    }
}
Benefits Explored:
Simplicity: The traditional iteration approach is straightforward and easy to understand, making it accessible to developers with varying levels of experience.
Early Exit: The loop breaks as soon as a mismatch is found, potentially saving computation time in case of large lists.
Â
 Approach 2: Java Streams
As we explore API response validation, we encounter Java Streams, a concept rooted in functional programming. Java Streams offer a unique way to handle matches in API response validation. In this section, we uncover the simplicity and advantages of using Java Streams.
// All imports here….
public class ApiResponseValidationWithStreams {
    public static void main(String[] args) {
        // API endpoint
        String apiEndpoint = "api_endpoint_here";
        // Perform the API request and get the response
        String response = RestAssured.given()
                .contentType(ContentType.JSON)
                .when()
                .get(apiEndpoint)
                .then()
                .extract().response().asString();
        // Our Expected alert codes
        List<String> expectedAlertCodes = Arrays.asList("Abc", "Bcd", "Cde", "Def");
        // Perform validation on the API response using Java streams
        validateApiResponseWithStreams(response, expectedAlertCodes);
    }
    private static void validateApiResponseWithStreams(String response, List<String> expectedAlertCodes) {
        // Extract the alert codes from the response using Java streams
        List<String> actualAlertCodes = RestAssured.given().body(response)
                .jsonPath().getList("data.country.state.city.alert");
        // Check if all expected codes are present in the response using streams
        boolean allCodesPresent = expectedAlertCodes.stream()
                .allMatch(actualAlertCodes::contains);
        // Print validation result
        if (allCodesPresent) {
            System.out.println("Validation successful: All expected alert codes found in the response.");
        } else {
            System.out.println("Validation failed: One or more expected alert codes not found in the response.");
        }
    }
}
Let's break down the key parts of the code:
1.
 // Our Expected alert codes
        List<String> expectedAlertCodes = Arrays.asList("Abc", "Bcd", "Cde", "Def");
Here,Â
The Arrays.asList method is used to create a List from an array. In this specific context, it's a convenient way to define a list of expected alert codes in a single line, rather than explicitly creating a new ArrayList<String> and adding each code individually, you can provide the codes directly within the Arrays.asList method.
This approach is cleaner and less error-prone When you're initializing a list with fixed values.
Extra Tip: If you want a modifiable list and not a fixed-size list, you should create a new ArrayList and add the elements from the array or another list. Here's how you can modify the code to use a modifiable list:
List<String> expectedAlertCodes = new ArrayList<>(Arrays.asList("Abc", "Bcd", "Cde", "Def"));
In this modified code, I use new ArrayList<>(Arrays.asList("Abc", "Bcd", "Cde", "Def")) to create a new ArrayList that is modifiable. The ArrayList constructor takes a Collection as an argument, and Arrays.asList returns a List, so it's a convenient way to create a modifiable list with the initial values. This allows you to later add or remove elements as needed.
2.
// Extract the alert codes from the response using Java streams
        List<String> actualAlertCodes = RestAssured.given().body(response)
                .jsonPath().getList("data.country.state.city.alert");
Here,Â
.getList()-Â is a JSONPath expression that is used to navigate the JSON structure and extract a list of values from the specified path.
In this specific case, it's extracting a list of alert codes from the JSON response. The structure of the path indicates that the alert codes are nested within "data," "country," "state," "city," and finally, "alert."
The result of this expression is a List<String> containing the values of the "alert" field for each city.
It starts at the root of the JSON structure.
Goes to the "data" field.
Within "data," goes to the "country" field.
Within "country," goes to the "state" field.
Within "state," goes to the "city" field.
Within "city," retrieves the values of the "alert" field for each city.
Collects these values into a List<String>.
So, It iterates over each JSON object representing a city, extracts the value of the "alert" field for each city, and stores these values in a List<String>. This list will contain all the alert codes for the cities in the response, and it can be further used for validation or analysis.
3.
boolean allCodesPresent = expectedAlertCodes.stream().allMatch(actualAlertCodes::contains);
Here,
This converts the expectedAlertCodes list into a stream, which is a sequence of elements supporting sequential and parallel aggregate operations.
.allMatch(actualAlertCodes::contains)
allMatch is a terminal operation in the Java Stream API. It returns true if all elements of the stream match the provided predicate, and false otherwise.
In this case, the predicate is actualAlertCodes::contains, which checks if each element from expectedAlertCodes is present in the actualAlertCodes list.
The :: operator is the method reference operator, and actualAlertCodes::contains is a shorthand notation for a lambda expression (code) -> actualAlertCodes.contains(code).
Further deep dive into the terminology,
Predicate- In the context of Java programming and the Stream API, a Predicate is a functional interface representing a boolean-valued function that takes an argument and returns true or false. It is often used for filtering elements in a collection or stream.
expectedAlertCodes::contains:Â
This is a method reference. It's a shorthand notation for a lambda expression. In this context, it's equivalent to (code) -> expectedAlertCodes.contains(code). The contains method is called on expectedAlertCodes for each element in actualAlertCodes to check if it is present in expectedAlertCodes.
Benefits:
Conciseness: Java streams provide a concise and expressive way to perform operations on collections, making the code cleaner and more readable.
Declarative Style: The use of streams promotes a more declarative coding style, focusing on "what to do" rather than "how to do it."
Parallelization: Streams offer built-in support for parallel processing, which can lead to improved performance on multicore processors.
Conclusion:
Both approaches achieve the same result — validating if all expected alert codes are present in the response. The choice between them depends on factors such as code readability, developer familiarity, and performance considerations.
By adopting a thoughtful approach to API response validation, we can enhance the reliability and maintainability of their test suites, contributing to overall software quality.
Comments