Most REST Assured tutorials show you a single given().when().then() against a sample API and call it done. That's fine for learning the syntax, but it doesn't cover what you actually need on a real project things like config per environment, clean test structure, and handling auth tokens that expire while the suite is running.
I'm Mukhammadjon Sanaev, a QA Automation Engineer in San Francisco. I've worked across e-commerce, logistics, and sports tech. This post walks through a simple REST Assured + TestNG + Maven setup I'd use on day one of a new API testing project, plus one problem I ran into on a real checkout API that isn't in the tutorials.
What We're Building
A small Java project that:
Uses REST Assured for API calls
Uses TestNG as the test runner
Runs against dev or staging with a single command
Handles an auth token that expires mid-run
Examples are based on an e-commerce checkout API — the kind of thing you'd test at a Shopify- or Wayfair-style company. Nothing proprietary, just the shape of a real checkout flow.
Project Structure
Keep it simple on day one:
api-tests/
├── pom.xml
├── testng.xml
└── src/test/
├── java/com/example/tests/
│ ├── BaseTest.java
│ ├── TokenManager.java
│ └── CheckoutTests.java
└── resources/
└── config.properties
Step 1: pom.xml
xml<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>api-tests</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.10.2</version>
</dependency>
</dependencies>
</project>
Step 2: Config File
Under src/test/resources/config.properties:
properties
base.url=https://api-dev.example.com
auth.url=https://auth-dev.example.com/oauth/token
client.id=your-client-id
client.secret=your-client-secret
Tip: don't commit real secrets to git. In a real project, read client.secret from an environment variable instead.
Step 3: BaseTest
Sets the base URL for every test:
javapublic class BaseTest {
@BeforeSuite
public void setup() {
RestAssured.baseURI = "https://api-dev.example.com";
}
}
Step 4: The Auth Problem
Here's the naive way to handle auth, which most tutorials show:
java// Fetch the token once, reuse forever
String token = given()
.auth().basic("client", "secret")
.post("/auth/token")
.jsonPath().getString("access_token");
This works for 10 tests. It breaks when your suite gets bigger.
The Problem
On one project, our API tokens expired after 15 minutes. Our regression suite took about 22 minutes to run. The first batch of tests passed fine, then around test 140 everything started failing with 401 Unauthorized — not because the code was wrong, but because the token had expired halfway through the run.
The fix isn't to make the suite shorter. The fix is making the framework aware that tokens expire.
The Fix: A Simple TokenManager
java
public class TokenManager {
private static String token;
private static Instant expiresAt;
public static String getToken() {
if (token == null || Instant.now().isAfter(expiresAt)) {
refresh();
}
return token;
}
private static void refresh() {
Response response = given()
.formParam("grant_type", "client_credentials")
.formParam("client_id", "your-client-id")
.formParam("client_secret", "your-client-secret")
.post("https://auth-dev.example.com/oauth/token");
token = response.jsonPath().getString("access_token");
int expiresIn = response.jsonPath().getInt("expires_in");
// Refresh 60 seconds early to avoid edge cases
expiresAt = Instant.now().plusSeconds(expiresIn - 60);
}
}
Two things worth noting:
The 60-second buffer. If you refresh exactly when the token expires, you can still hit a race condition with the server clock. Refreshing a bit early avoids that.
It only refreshes when needed. Most tests just grab the cached token.
Using It
Every API call pulls a fresh token through TokenManager:
javapublic class CheckoutTests extends BaseTest {
@Test
public void getCart_returnsItems() {
given()
.header("Authorization", "Bearer " + TokenManager.getToken())
.pathParam("cartId", "cart-123")
.when()
.get("/cart/{cartId}")
.then()
.statusCode(200)
.body("items", hasSize(greaterThan(0)));
}
}
Step 5: Running It
testng.xml:
xml
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="API Tests">
<test name="Checkout">
<classes>
<class name="com.example.tests.CheckoutTests"/>
</classes>
</test>
</suite>
Run:
bash
mvn test
That's it a working API test suite that doesn't fall over when tokens expire.
What I'd Add Next
This is a starting point, not the finished framework. Once the basics work, I'd add:
Separate config files for dev, staging, and prod
JSON schema validation on responses
An HTML report like Allure or Extent Reports
CI/CD integration with Jenkins or GitHub Actions
The Takeaway
The tricky parts of API automation aren't the tools REST Assured, TestNG, and Maven are straightforward once you've set them up once. The tricky parts are the problems that only show up when a real suite runs against a real API: auth tokens expiring, environment config drift, response schemas changing silently.
If you're setting this up for the first time, start small. Get one test running, handle auth properly, then grow from there.
Top comments (0)