REST Assured Tutorial: API Testing in Java (2026)
Quick answer
REST Assured is a Java library that gives API tests a fluent given().when().then() syntax, so a test reads like the request it makes instead of manually building an HttpClient. Pair it with JUnit 5 for the runner and parametrization, and json-schema-validator for response-structure validation instead of field-by-field assertions. It is the closest Java equivalent to Python's pytest + requests, and integrates the same way — as a normal Maven or Gradle test dependency, not a separate tool.
Reviewed by Parveen Kumari
REST Assured is a Java library for testing REST APIs with a fluent, readable syntax — given() sets up the request, when() fires it, and then() asserts on the response — so a test reads as a description of the HTTP interaction it performs, not a wall of HttpClient boilerplate. It is the closest thing Java has to Python's pytest + requests combination, and it integrates the same way: as a normal test dependency that runs inside your existing Maven or Gradle build.
This guide builds a real REST Assured + JUnit 5 suite from scratch: project setup, the given-when-then DSL, JSON schema validation, parametrized tests across multiple endpoints, and a GitHub Actions workflow that runs mvn test on every push. Every code sample runs as written against JSONPlaceholder, a free public fake REST API.
Table of Contents
- Why REST Assured for Java API Testing
- What You Need
- Project Structure
- Writing Your First REST Assured Test
- Validating Response Schemas
- Testing POST, PUT, and DELETE Requests
- Parametrized Tests with JUnit 5
- Shared Setup with @BeforeAll and RequestSpecification
- Running Your Suite in CI/CD with GitHub Actions
- Common Pitfalls in REST Assured Suites
- REST Assured vs Postman vs pytest vs AI-Generated Tests
- When to Move Beyond Hand-Written Tests
- FAQ
Why REST Assured for Java API Testing
Testing a REST API in plain Java without a library means building the request with HttpClient or OkHttp, manually parsing the JSON response, and asserting on individual fields with a general-purpose assertion library. REST Assured collapses all three steps into one chained expression, and integrates Hamcrest matchers and JsonPath so you can assert on nested JSON fields ("address.city") without deserializing into a POJO first.
Because it is a plain JVM library — not a separate application — it runs inside the same mvn test or gradle test command as your unit tests, in the same CI job, reviewed in the same pull requests as the rest of your Java code.
What You Need
- Java 17+ (this guide targets a current LTS release)
- Maven (Gradle works identically; this guide uses Maven)
- JUnit 5 — the test runner
- REST Assured — the request/response DSL
- json-schema-validator — REST Assured's schema validation module
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Check Maven Central for the current release of each artifact before pinning — these versions were current as of this guide's publication.
Project Structure
api-tests/
├── pom.xml
└── src
└── test
├── java
│ └── com/example/apitests/
│ ├── UsersApiTest.java
│ └── PostsApiTest.java
└── resources
└── schemas/
└── user-schema.json
Maven's convention places JSON Schema files under src/test/resources so they land on the test classpath automatically — matchesJsonSchemaInClasspath(), used below, depends on that.
Writing Your First REST Assured Test
// UsersApiTest.java
package com.example.apitests;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
class UsersApiTest {
@BeforeAll
static void setup() {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
void getUserReturns200() {
given()
.when()
.get("/users/1")
.then()
.statusCode(200);
}
@Test
void getUserReturnsExpectedFields() {
given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.body("id", equalTo(1))
.body("email", containsString("@"))
.body("name", not(emptyString()));
}
}
Run it:
mvn test
given(), when(), and then() are not just readable naming — each returns a builder scoped to its stage, so a request specification (headers, auth, base URI) set in given() cannot accidentally leak into the assertion stage in then().
Validating Response Schemas
Field-by-field assertions do not catch a field silently changing type or an unexpected field appearing. json-schema-validator checks the whole response shape in one assertion.
src/test/resources/schemas/user-schema.json:
Ready to shift left with your API testing?
Try our no-code API test automation platform free. Generate tests from OpenAPI, run in CI/CD, and scale quality.
{
"type": "object",
"required": ["id", "name", "username", "email"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"email": { "type": "string", "format": "email" },
"address": { "type": "object" },
"phone": { "type": "string" },
"website": { "type": "string" },
"company": { "type": "object" }
}
}
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
@Test
void userMatchesSchema() {
given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/user-schema.json"));
}
Testing POST, PUT, and DELETE Requests
// PostsApiTest.java
package com.example.apitests;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
class PostsApiTest {
@BeforeAll
static void setup() {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
void createPost() {
String payload = "{ \"title\": \"foo\", \"body\": \"bar\", \"userId\": 1 }";
given()
.contentType("application/json")
.body(payload)
.when()
.post("/posts")
.then()
.statusCode(201)
.body("title", equalTo("foo"))
.body("id", notNullValue());
}
@Test
void updatePost() {
String payload = "{ \"id\": 1, \"title\": \"updated\", \"body\": \"bar\", \"userId\": 1 }";
given()
.contentType("application/json")
.body(payload)
.when()
.put("/posts/1")
.then()
.statusCode(200)
.body("title", equalTo("updated"));
}
@Test
void deletePost() {
given()
.when()
.delete("/posts/1")
.then()
.statusCode(200);
}
}
For anything beyond a trivial inline string, build the request body from a Java object (a POJO or record) and let REST Assured serialize it with Jackson or Gson on the classpath, rather than hand-writing JSON strings — it keeps the payload type-checked and refactor-safe.
Parametrized Tests with JUnit 5
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
class UserStatusCodeTest {
@ParameterizedTest
@CsvSource({
"1, 200",
"10, 200",
"999, 404"
})
void getUserStatusCodes(int userId, int expectedStatus) {
given()
.baseUri("https://jsonplaceholder.typicode.com")
.when()
.get("/users/" + userId)
.then()
.statusCode(expectedStatus);
}
}
@CsvSource keeps the input table next to the test instead of in a separate data file, and JUnit 5 reports each row as its own pass/fail case — the 999 → 404 row proves the API rejects an invalid ID correctly, which a suite that only tests valid IDs would never catch.
Shared Setup with @BeforeAll and RequestSpecification
Repeating .baseUri(...), headers, and auth on every test invites drift. A RequestSpecification centralizes them once:
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.BeforeAll;
import static io.restassured.RestAssured.given;
class AuthenticatedApiTest {
static RequestSpecification spec;
@BeforeAll
static void setup() {
spec = new RequestSpecBuilder()
.setBaseUri("https://jsonplaceholder.typicode.com")
.addHeader("Authorization", "Bearer " + System.getenv("TEST_API_TOKEN"))
.build();
}
@Test
void getProtectedResource() {
given()
.spec(spec)
.when()
.get("/account")
.then()
.statusCode(200);
}
}
Reading the token from System.getenv("TEST_API_TOKEN") keeps it out of the repository — set it as an encrypted secret in your CI provider, never as a literal string in a test file.
Running Your Suite in CI/CD with GitHub Actions
# .github/workflows/api-tests.yml
name: API Tests
on: [push, pull_request]
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'maven'
- run: mvn -B test
- uses: actions/upload-artifact@v4
if: always()
with:
name: surefire-reports
path: target/surefire-reports/
cache: 'maven' caches ~/.m2/repository between runs, so dependency resolution does not re-download on every build. if: always() on the upload step ensures the Surefire report artifact is available specifically on the runs that fail — see our step-by-step CI/CD guide for the equivalent GitLab CI and Jenkins configuration.
Free Guided worksheet
Build Your Testing Strategy in 30 Minutes
A structured worksheet that walks you through defining your testing strategy in 30 minutes. Cover architecture, tools, layers, and team responsibilities.
Download FreeCommon Pitfalls in REST Assured Suites
- Setting
RestAssured.baseURIas a static field shared across test classes. It works but creates ordering-dependent tests if any class changes it; prefer aRequestSpecificationbuilt per test class. - Asserting only the status code. Validate structure with
matchesJsonSchemaInClasspath(), not just individual.body("field", ...)checks. - Hand-building JSON strings for request bodies. Serialize a Java object instead — string-built JSON breaks silently on a missed comma or unescaped quote.
- No negative-path coverage. Parametrize invalid IDs and malformed payloads alongside valid ones, the same way you would in pytest.
- Ignoring response time. REST Assured supports
.time(lessThan(2000L, TimeUnit.MILLISECONDS))in the.then()chain — a cheap way to catch a latency regression in the same test that already checks correctness. - Committing tokens or passwords into test source. Read them from environment variables set as CI secrets.
REST Assured vs Postman vs pytest vs AI-Generated Tests
| Approach | Language | Learning curve | CI/CD fit | Schema validation | Best for |
|---|---|---|---|---|---|
| REST Assured | Java | Medium | Native — same runner as JUnit/TestNG | Automatic (json-schema-validator) | Java/JVM teams |
| pytest + requests | Python | Low | Native — same runner as unit tests | Manual (jsonschema) | Python teams |
| Postman + Newman | JSON collections, JS scripts | Low for exploration, higher for CI | Requires exporting/running via Newman CLI | Manual (pm.test scripts) | Manual exploration, small automated suites |
| AI-generated (Total Shift Left) | None required | Low — import a spec | Native CI plugins | Automatic — generated from the OpenAPI schema | Teams testing many endpoints across services |
REST Assured and pytest occupy the same role for their respective ecosystems: both run as a normal test dependency inside the language's existing test runner, rather than as a separate application. Postman remains strongest for exploring an API before a test exists — see our full tools comparison for the wider field.
When to Move Beyond Hand-Written Tests
REST Assured's readability does not change the underlying economics: twice the endpoints means roughly twice the test methods, schema files, and request specifications to maintain. For a service with a dozen endpoints, that is a reasonable trade. Somewhere in the range of a few dozen endpoints across multiple services, the test suite's maintenance cost starts competing with the API's own development for engineering time.
The alternative is generating the suite directly from the OpenAPI specification instead of hand-translating it into Java. Total Shift Left imports your spec, generates positive, negative, and boundary test cases for every endpoint, and re-generates automatically when the spec changes — so schema drift updates the suite instead of silently breaking it.
Frequently Asked Questions
What is REST Assured used for?
Testing REST APIs from Java, with a fluent given().when().then() syntax for building requests and asserting on status codes, headers, and JSON or XML body content.
Do I need Postman if I use REST Assured? For ad-hoc exploration, Postman is faster. For automated, CI/CD-integrated regression testing, REST Assured runs in the same Maven or Gradle command as your unit tests with no separate tool.
How do I validate a JSON schema with REST Assured?
Add the io.rest-assured:json-schema-validator dependency, place the schema file under src/test/resources, and call .body(matchesJsonSchemaInClasspath("schema.json")) inside .then().
REST Assured vs Postman — which is better for CI/CD?
REST Assured. It runs via mvn test or gradle test, the same command your CI pipeline already runs for unit tests, with no Newman CLI step or collection export required.
Can REST Assured test SOAP APIs as well as REST? It can send and assert on XML bodies including SOAP envelopes via its XmlPath support, but it does not manage WSDL contracts the way a dedicated SOAP client does.
Can a REST Assured suite fully replace an AI-generated API test suite? For a handful of endpoints, hand-written REST Assured is often faster to set up. Past a few dozen endpoints across services, generating the suite from an OpenAPI spec keeps coverage complete without the manual upkeep.
Key Takeaways
- REST Assured's given-when-then DSL replaces raw HttpClient boilerplate and runs as a normal Maven or Gradle test dependency.
json-schema-validatorcatches structural drift that field-by-field.body("field", ...)assertions miss.@ParameterizedTestwith@CsvSourcecovers negative paths cheaply — invalid IDs belong in the same table as valid ones.- A shared
RequestSpecificationcentralizes base URI, headers, and auth instead of repeating them per test class. mvn testin CI needs no separate tool — it is the same command your unit tests already run under.- Hand-written suites scale linearly with endpoint count. Past a few dozen endpoints across services, generating tests from an OpenAPI spec becomes the more maintainable path.
Related Articles
- API Testing with Python: pytest + requests Tutorial — the equivalent stack for Python teams.
- How AI Generates API Tests from OpenAPI — automate test creation directly from your spec.
- API Test Automation with CI/CD: Step-by-Step Guide — wire any test suite into your pipeline.
- REST API Testing Best Practices — language-agnostic principles this tutorial applies in Java.
Generate the Equivalent Suite from Your OpenAPI Spec
Every test in this guide can also be generated automatically. Import your OpenAPI specification into Total Shift Left and get positive, negative, and boundary test cases for every endpoint — regenerated the moment your spec changes, with no REST Assured classes to maintain by hand.
Start your free trial and compare the generated suite against your own, or see plans and pricing if you're already evaluating.
Ready to shift left with your API testing?
Try our no-code API test automation platform free.