Introduction

In our REST Assured series, we’ve explored topics like Handling Timeouts, Custom Assertions, and POJO Mapping, building a robust toolkit for API testing. Now, we’ll dive into Asynchronous Requests, a technique for testing APIs that process requests non-blocking or return responses after a delay. This guide demonstrates how to handle asynchronous requests with REST Assured, with practical examples and best practices. It’s designed for beginners and experienced developers, ensuring your tests effectively manage async API behavior.

Key Point: Handling asynchronous requests in REST Assured ensures tests can manage delayed or non-blocking API responses, improving test reliability and efficiency.

What are Asynchronous Requests?

Asynchronous Requests occur when an API accepts a request and processes it in the background, either returning an immediate acknowledgment (e.g., 202 Accepted) or requiring the client to poll for results. Common scenarios include:

  • Background Processing: APIs that queue tasks (e.g., image processing, report generation).
  • Long-Running Operations: APIs with delayed responses due to complex computations.
  • Event-Driven Systems: APIs that trigger events and provide status updates via polling or callbacks.

REST Assured supports async testing through:

  • Polling: Repeatedly checking a status endpoint until a result is available.
  • Async HTTP Client: Configuring non-blocking requests (though less common in tests).
  • Timeout Management: Handling delays with appropriate timeouts.
We’ll use https://httpbin.org/delay to simulate delayed responses and https://jsonplaceholder.typicode.com for standard API testing, demonstrating polling and async strategies.

Setting Up for Asynchronous Requests

We’ll set up a Maven project with REST Assured, JUnit 5, and Allure for reporting, consistent with previous posts. No additional dependencies are needed for async handling, as REST Assured’s default HTTP client supports polling and timeouts.

Here’s the pom.xml, styled with your preferred Blogger format for XML syntax highlighting:



    4.0.0
    com.example
    rest-assured-async
    1.0-SNAPSHOT

    
        11
        2.27.0
        1.9.22
    

    
        
            io.rest-assured
            rest-assured
            5.4.0
            test
        
        
            org.junit.jupiter
            junit-jupiter
            5.10.2
            test
        
        
            org.hamcrest
            hamcrest
            2.2
            test
        
        
            com.fasterxml.jackson.core
            jackson-databind
            2.15.2
            test
        
        
            io.qameta.allure
            allure-junit5
            ${allure.version}
            test
        
    

    
        
            
                org.apache.maven.plugins
                maven-surefire-plugin
                3.2.5
                
                    
                        -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
                    
                
                
                    
                        org.aspectj
                        aspectjweaver
                        ${aspectj.version}
                    
                
            
            
                io.qameta.allure
                allure-maven
                2.12.0
                
                    ${allure.version}
                
            
        
    

No additional setup is required for https://httpbin.org or https://jsonplaceholder.typicode.com. We’ll use polling and timeout configurations in the examples.

Implementing Asynchronous Request Handling

Let’s explore how to handle asynchronous requests with REST Assured, covering polling for results, handling async responses, timeout integration, and Allure reporting.

Example 1: Polling for Async Response

Poll a status endpoint until the result is available.


import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;

@Feature("Asynchronous Requests with REST Assured")
public class PollingTest {

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "https://httpbin.org";
    }

    @Test
    @DisplayName("Test polling for async response")
    @Description("Test polling a delayed API until response is available")
    public void testPolling() {
        int maxAttempts = 5;
        int pollIntervalMs = 1000; // 1 second
        boolean isComplete = false;
        Response response = null;

        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            response = given()
                .log().all()
                .config(config().httpClient(httpClientConfig()
                    .setParam("http.socket.timeout", 2000)))
            .when()
                .get("/delay/2") // 2-second delay
            .then()
                .log().all()
                .extract().response();

            if (response.statusCode() == 200) {
                isComplete = true;
                break;
            }

            try {
                Thread.sleep(pollIntervalMs);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        assertTrue(isComplete, "API did not respond successfully after " + maxAttempts + " attempts");
        assertEquals(200, response.statusCode(), "Status code should be 200");
        assertNotNull(response.path("url"), "Response should contain URL");
    }
}

Explanation:

  • Polls /delay/2 (2-second delay) up to 5 times, with 1-second intervals.
  • Checks for a 200 status code to confirm completion.
  • Sets a 2-second read timeout to avoid hanging on each attempt.
  • Breaks the loop on success or fails after max attempts.
Important: Use polling for APIs that require checking a status endpoint repeatedly until a result is available.

Example 2: Handling Async API with Status Check

Test an API that returns a 202 Accepted status and requires polling a status endpoint.


import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;

@Feature("Asynchronous Requests with REST Assured")
public class AsyncStatusCheckTest {

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "https://httpbin.org";
    }

    @Test
    @DisplayName("Test async API with status check")
    @Description("Test an async API with 202 Accepted and status polling")
    public void testAsyncStatusCheck() {
        // Simulate an async API that returns 202 Accepted
        Response initialResponse = given()
            .log().all()
        .when()
            .post("/status/202")
        .then()
            .log().all()
            .statusCode(202)
            .extract().response();

        // Poll a related endpoint (simulating status check)
        int maxAttempts = 3;
        int pollIntervalMs = 1000;
        boolean isComplete = false;
        Response statusResponse = null;

        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            statusResponse = given()
                .log().all()
                .config(config().httpClient(httpClientConfig()
                    .setParam("http.socket.timeout", 2000)))
            .when()
                .get("/delay/1") // Simulate status endpoint with 1s delay
            .then()
                .log().all()
                .extract().response();

            if (statusResponse.statusCode() == 200) {
                isComplete = true;
                break;
            }

            try {
                Thread.sleep(pollIntervalMs);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        assertTrue(isComplete, "Status check did not complete after " + maxAttempts + " attempts");
        assertEquals(200, statusResponse.statusCode(), "Status code should be 200");
    }
}

Explanation:

  • Simulates an async API with /status/202 (returns 202 Accepted).
  • Polls /delay/1 as a status endpoint up to 3 times.
  • Verifies the initial 202 status and final 200 status.

Example 3: Using Awaitility for Polling

Use the Awaitility library for more elegant polling in async tests.

Update pom.xml to include Awaitility:



    org.awaitility
    awaitility
    4.2.1
    test

Test code:


import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;

@Feature("Asynchronous Requests with REST Assured")
public class AwaitilityPollingTest {

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "https://httpbin.org";
    }

    @Test
    @DisplayName("Test polling with Awaitility")
    @Description("Test polling a delayed API using Awaitility")
    public void testAwaitilityPolling() {
        Response[] response = new Response[1];

        Awaitility.await()
            .atMost(Duration.ofSeconds(5))
            .pollInterval(Duration.ofSeconds(1))
            .until(() -> {
                response[0] = given()
                    .log().all()
                    .config(config().httpClient(httpClientConfig()
                        .setParam("http.socket.timeout", 2000)))
                .when()
                    .get("/delay/2")
                .then()
                    .log().all()
                    .extract().response();
                return response[0].statusCode() == 200;
            });

        assertEquals(200, response[0].statusCode(), "Status code should be 200");
        assertNotNull(response[0].path("url"), "Response should contain URL");
    }
}

Explanation:

  • Awaitility.await(): Polls until the condition (status code 200) is met.
  • atMost: Sets a 5-second maximum wait time.
  • pollInterval: Polls every 1 second.
  • More concise and readable than manual polling loops.
Pro Tip: Use Awaitility for complex polling scenarios to simplify code and improve test maintainability.

Example 4: Testing Async POST with Polling

Test an async POST request with polling for results.


import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;

@Feature("Asynchronous Requests with REST Assured")
public class AsyncPostTest {

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
    }

    @Test
    @DisplayName("Test async POST with polling")
    @Description("Test async POST with simulated polling for result")
    public void testAsyncPost() {
        // Simulate async POST
        String requestBody = "{\"title\": \"Test Post\", \"body\": \"This is a test\", \"userId\": 1}";
        Response initialResponse = given()
            .header("Content-Type", "application/json")
            .body(requestBody)
            .log().all()
        .when()
            .post("/posts")
        .then()
            .log().all()
            .statusCode(201)
            .extract().response();

        // Simulate polling for result (using the created post ID)
        int postId = initialResponse.path("id");
        int maxAttempts = 3;
        int pollIntervalMs = 1000;
        boolean isComplete = false;
        Response resultResponse = null;

        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            resultResponse = given()
                .log().all()
                .config(config().httpClient(httpClientConfig()
                    .setParam("http.socket.timeout", 2000)))
            .when()
                .get("/posts/{id}", postId)
            .then()
                .log().all()
                .extract().response();

            if (resultResponse.statusCode() == 200 && resultResponse.path("title").equals("Test Post")) {
                isComplete = true;
                break;
            }

            try {
                Thread.sleep(pollIntervalMs);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        assertTrue(isComplete, "Post was not available after " + maxAttempts + " attempts");
        assertEquals(200, resultResponse.statusCode(), "Status code should be 200");
        assertEquals("Test Post", resultResponse.path("title"), "Title should match");
    }
}

Explanation:

  • Simulates an async POST to /posts (returns 201 Created).
  • Polls the created post’s endpoint to verify availability.
  • Checks for the correct title to confirm the result.

Example 5: Async Handling with Allure

Integrate async request handling with Allure reporting.


import io.qameta.allure.Allure;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static io.restassured.RestAssured.*;
import static org.junit.jupiter.api.Assertions.*;

@Feature("Asynchronous Requests with REST Assured")
public class AllureAsyncTest {

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "https://httpbin.org";
    }

    @Test
    @DisplayName("Test async polling with Allure")
    @Description("Test polling a delayed API with Allure reporting")
    public void testAsyncWithAllure() {
        int maxAttempts = 5;
        int pollIntervalMs = 1000;
        boolean isComplete = false;
        Response response = null;
        int attempt;

        for (attempt = 1; attempt <= maxAttempts; attempt++) {
            response = given()
                .log().all()
                .config(config().httpClient(httpClientConfig()
                    .setParam("http.socket.timeout", 2000)))
            .when()
                .get("/delay/2")
            .then()
                .log().all()
                .extract().response();

            Allure.addAttachment("Attempt " + attempt + " Response", "application/json", 
                response.asString(), ".json");

            if (response.statusCode() == 200) {
                isComplete = true;
                break;
            }

            try {
                Thread.sleep(pollIntervalMs);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        assertTrue(isComplete, "API did not respond successfully after " + maxAttempts + " attempts");
        assertEquals(200, response.statusCode(), "Status code should be 200");
    }
}

Explanation:

  • Attaches each polling attempt’s response to Allure for debugging.
  • Logs request and response details with log().all().
  • Run mvn clean test and mvn allure:serve to view the report.

Integrating with CI/CD

Add async request tests to a GitHub Actions pipeline for automation.

Create or update .github/workflows/ci.yml:


name: REST Assured Async CI Pipeline

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 11
        uses: actions/setup-java@v4
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Cache Maven dependencies
        uses: actions/cache@v4
        with:
          path: ~/.m2/repository
          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-maven-

      - name: Run tests
        run: mvn clean test

      - name: Generate Allure report
        run: mvn allure:report

      - name: Upload Allure results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: allure-results
          path: target/allure-results

      - name: Publish Allure report
        if: always()
        uses: simple-elf/allure-report-action@v1.7
        with:
          allure_results: target/allure-results
          gh_pages: gh-pages
          allure_report: allure-report

      - name: Deploy report to GitHub Pages
        if: always()
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: allure-report

Explanation:

  • Runs async request tests with mvn clean test.
  • Publishes Allure reports to GitHub Pages for visibility.

Tips for Beginners

  • Optimize Polling: Set reasonable poll intervals and max attempts to balance test speed and reliability.
  • Use Awaitility: Prefer Awaitility for complex polling to simplify code.
  • Manage Timeouts: Configure timeouts to prevent tests from hanging on slow async APIs.
  • Log Details: Enable log().all() to debug async test failures.
Troubleshooting Tip: If an async test fails, check logs for response details, verify polling conditions, and ensure timeouts are sufficient for the API’s delay.

What’s Next?

This post enhances our REST Assured series by introducing Asynchronous Requests, a key technique for testing non-blocking APIs. To continue your learning, consider exploring:

  • GraphQL Testing: Testing GraphQL APIs with REST Assured.
  • Advanced Allure Reporting: Customizing reports with environments and categories.
  • End-to-End Testing: Combining REST Assured with Selenium for UI and API testing.
Stay tuned for more testing tutorials!