Introduction
In our REST Assured series, we’ve explored topics like POJO Mapping, Parameterized Tests, and REST Assured with JUnit, equipping you with tools to create robust API tests. Now, we’ll dive into Custom Assertions, a technique to create tailored validation logic for API responses, improving test readability and maintainability. This guide demonstrates how to implement custom assertions with REST Assured, with practical examples and best practices. It’s designed for beginners and experienced developers, ensuring you can validate complex API responses effectively.
Key Point: Custom Assertions in REST Assured encapsulate complex validation logic into reusable methods, enhancing test clarity and reducing code duplication.
What are Custom Assertions?
Custom Assertions involve creating user-defined methods or classes to validate API responses beyond standard assertions (e.g., Hamcrest Matchers or JUnit assertions). They are useful for:
- Complex Logic: Validating nested JSON structures or business rules.
- Reusability: Applying the same validation across multiple tests.
- Readability: Making tests more expressive with domain-specific assertions.
- Error Reporting: Providing detailed failure messages for debugging.
REST Assured supports custom assertions by extracting responses (e.g., as POJOs, JSON paths, or raw strings) and validating them using Java logic. We’ll use https://jsonplaceholder.typicode.com
for examples, testing endpoints like /posts
and /comments
to demonstrate custom assertions for single objects, collections, and complex validations.
Setting Up for Custom Assertions
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 custom assertions, as they rely on Java and existing libraries like Jackson.
Here’s the pom.xml
, styled with your preferred Blogger format for XML syntax highlighting:
4.0.0
com.example
rest-assured-custom-assertions
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://jsonplaceholder.typicode.com
. We’ll create custom assertion methods and reuse the Post
POJO from the previous post for some examples.
Implementing Custom Assertions
Let’s explore how to implement custom assertions with REST Assured, covering simple validations, POJO-based assertions, collection validations, and reusable assertion classes.
Example 1: Simple Custom Assertion Method
Create a custom assertion method to validate a post’s properties.
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 static io.restassured.RestAssured.*;
import static org.junit.jupiter.api.Assertions.*;
@Feature("Custom Assertions with REST Assured")
public class SimpleCustomAssertionTest {
@BeforeEach
public void setup() {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
private void assertPostResponse(Response response, int expectedId) {
assertEquals(200, response.getStatusCode(), "Status code should be 200");
assertEquals(expectedId, response.path("id"), "Post ID should match");
assertNotNull(response.path("title"), "Title should not be null");
assertTrue(response.path("userId") > 0, "User ID should be positive");
}
@Test
@DisplayName("Test custom assertion for a single post")
@Description("Test using a custom assertion method for a post")
public void testCustomAssertion() {
Response response = given()
.log().all()
.when()
.get("/posts/1")
.then()
.log().all()
.extract().response();
assertPostResponse(response, 1);
}
}
Explanation:
assertPostResponse
: A custom method that validates status code, ID, title, and user ID.- Extracts the response and passes it to the custom assertion method.
- JUnit assertions provide detailed failure messages.
Important: Keep custom assertion methods focused and reusable to maintain test clarity.
Example 2: Custom Assertion with POJO
Use a POJO-based custom assertion for a post.
Reuse the Post
POJO from the previous post:
public class Post {
private int id;
private int userId;
private String title;
private String body;
public Post() {}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public int getUserId() { return userId; }
public void setUserId(int userId) { this.userId = userId; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getBody() { return body; }
public void setBody(String body) { this.body = body; }
}
Test code:
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.junit.jupiter.api.Assertions.*;
@Feature("Custom Assertions with REST Assured")
public class POJOCustomAssertionTest {
@BeforeEach
public void setup() {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
private void assertValidPost(Post post, int expectedId) {
assertEquals(expectedId, post.getId(), "Post ID should match");
assertTrue(post.getUserId() > 0, "User ID should be positive");
assertNotNull(post.getTitle(), "Title should not be null");
assertFalse(post.getBody().isEmpty(), "Body should not be empty");
}
@Test
@DisplayName("Test custom assertion with POJO")
@Description("Test using a custom assertion for a POJO-mapped post")
public void testPOJOCustomAssertion() {
Post post = given()
.log().all()
.when()
.get("/posts/1")
.then()
.log().all()
.statusCode(200)
.extract().as(Post.class);
assertValidPost(post, 1);
}
}
Explanation:
assertValidPost
: Validates aPost
POJO’s properties.- Combines POJO mapping with custom assertions for readable validation.
- Assertions focus on business-relevant properties.
Example 3: Custom Assertion for Collections
Validate a list of posts with a custom assertion.
Test code:
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static io.restassured.RestAssured.*;
import static org.junit.jupiter.api.Assertions.*;
@Feature("Custom Assertions with REST Assured")
public class CollectionCustomAssertionTest {
@BeforeEach
public void setup() {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
private void assertValidPostList(List posts) {
assertEquals(100, posts.size(), "Should return 100 posts");
assertTrue(posts.stream().allMatch(post -> post.getId() > 0), "All post IDs should be positive");
assertTrue(posts.stream().allMatch(post -> post.getTitle() != null), "All titles should be non-null");
assertTrue(posts.stream().noneMatch(post -> post.getBody().isEmpty()), "No post bodies should be empty");
}
@Test
@DisplayName("Test custom assertion for a list of posts")
@Description("Test using a custom assertion for a list of POJO-mapped posts")
public void testCollectionCustomAssertion() {
List posts = given()
.log().all()
.when()
.get("/posts")
.then()
.log().all()
.statusCode(200)
.extract().jsonPath().getList("", Post.class);
assertValidPostList(posts);
}
}
Explanation:
assertValidPostList
: Validates a list ofPost
POJOs using stream operations.- Checks list size and properties across all elements.
- Uses
jsonPath().getList
to map the response array to a list of POJOs.
Pro Tip: Use Java streams or loops in custom assertions for collections to validate properties efficiently.
Example 4: Reusable Custom Assertion Class
Create a reusable assertion class for post validations.
Assertion class:
import org.junit.jupiter.api.Assertions;
public class PostAssertions {
private final Post post;
private PostAssertions(Post post) {
this.post = post;
}
public static PostAssertions assertThat(Post post) {
return new PostAssertions(post);
}
public PostAssertions hasId(int expectedId) {
Assertions.assertEquals(expectedId, post.getId(), "Post ID should match");
return this;
}
public PostAssertions hasValidUserId() {
Assertions.assertTrue(post.getUserId() > 0, "User ID should be positive");
return this;
}
public PostAssertions hasNonEmptyTitle() {
Assertions.assertNotNull(post.getTitle(), "Title should not be null");
Assertions.assertFalse(post.getTitle().isEmpty(), "Title should not be empty");
return this;
}
}
Test code:
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
@Feature("Custom Assertions with REST Assured")
public class CustomAssertionClassTest {
@BeforeEach
public void setup() {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
@DisplayName("Test custom assertion class for a post")
@Description("Test using a custom assertion class for a POJO-mapped post")
public void testCustomAssertionClass() {
Post post = given()
.log().all()
.when()
.get("/posts/1")
.then()
.log().all()
.statusCode(200)
.extract().as(Post.class);
PostAssertions.assertThat(post)
.hasId(1)
.hasValidUserId()
.hasNonEmptyTitle();
}
}
Explanation:
PostAssertions
: A fluent assertion class with chainable methods.assertThat
: Entry point for assertions, mimicking Hamcrest or AssertJ style.- Reusable across tests for consistent post validations.
Example 5: Custom Assertions with Allure
Integrate custom assertions with Allure reporting.
import io.qameta.allure.Allure;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import static io.restassured.RestAssured.*;
import static org.junit.jupiter.api.Assertions.*;
@Feature("Custom Assertions with REST Assured")
public class AllureCustomAssertionTest {
@BeforeEach
public void setup() {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
private void assertValidPostWithAllure(Post post, int expectedId) throws IOException {
assertEquals(expectedId, post.getId(), "Post ID should match");
assertNotNull(post.getTitle(), "Title should not be null");
// Attach POJO to Allure
ObjectMapper mapper = new ObjectMapper();
String postJson = mapper.writeValueAsString(post);
Allure.addAttachment("Validated Post", "application/json", postJson, ".json");
}
@Test
@DisplayName("Test custom assertion with Allure")
@Description("Test custom assertion with Allure reporting")
public void testCustomAssertionWithAllure() throws IOException {
Post post = given()
.log().all()
.when()
.get("/posts/1")
.then()
.log().all()
.statusCode(200)
.extract().as(Post.class);
assertValidPostWithAllure(post, 1);
}
}
Explanation:
assertValidPostWithAllure
: Combines assertions with Allure attachment.ObjectMapper
: Serializes the POJO for reporting.- Run
mvn clean test
andmvn allure:serve
to view the report.
Integrating with CI/CD
Add custom assertion tests to a GitHub Actions pipeline for automation.
Create or update .github/workflows/ci.yml
:
name: REST Assured Custom Assertions 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 custom assertion tests with
mvn clean test
. - Publishes Allure reports to GitHub Pages for visibility.
Tips for Beginners
- Start Simple: Begin with custom assertion methods before creating complex classes.
- Use Descriptive Messages: Include clear failure messages in assertions for debugging.
- Combine with POJOs: Leverage POJOs for structured data in custom assertions.
- Enable Logging: Use
log().all()
to debug failed assertions.
Troubleshooting Tip: If a custom assertion fails, enable log().all()
to inspect the response and verify the assertion logic against the actual data.
What’s Next?
This post enhances our REST Assured series by introducing Custom Assertions, a powerful tool for tailored API validations. 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.