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