Getting a token is easy. Managing tokens across 200 test methods is where it gets messy. If every test method calls the login endpoint, your test suite takes 10 minutes instead of 2. If you share a token incorrectly, tests fail randomly when the token expires mid-run. Token management is what separates a beginner framework from a production framework.
Login ONCE in @BeforeSuite or @BeforeClass — get the token
Store the token in a shared variable or config object
Every test method reads from this shared store
Check token expiry before each test (or each class) — refresh if needed
For multi-role tests, get separate tokens for each role in setup
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import org.testng.annotations.BeforeSuite;
import static io.restassured.RestAssured.*;
public class BaseTest {
protected static String adminToken;
protected static String userToken;
private static long tokenExpiryTime;
@BeforeSuite
public void globalSetup() {
RestAssured.baseURI = System.getenv("API_BASE_URL");
if (RestAssured.baseURI == null) {
RestAssured.baseURI = "https://api.example.com";
}
// Get tokens for different roles
adminToken = loginAndGetToken("admin@example.com", "Admin@1234");
userToken = loginAndGetToken("user@example.com", "User@1234");
// Set expiry 55 min from now (tokens usually last 1 hour)
tokenExpiryTime = System.currentTimeMillis() + (55 * 60 * 1000);
}
private String loginAndGetToken(String email, String password) {
return given()
.contentType(ContentType.JSON)
.body(String.format("""
{
"email": "%s",
"password": "%s"
}
""", email, password))
.when()
.post("/auth/login")
.then()
.statusCode(200)
.extract()
.jsonPath()
.getString("token");
}
// Call this at the start of any test that runs long
protected void refreshTokenIfExpired() {
if (System.currentTimeMillis() > tokenExpiryTime) {
adminToken = loginAndGetToken("admin@example.com", "Admin@1234");
userToken = loginAndGetToken("user@example.com", "User@1234");
tokenExpiryTime = System.currentTimeMillis() + (55 * 60 * 1000);
}
}
// Convenience method — admin request
protected RequestSpecification asAdmin() {
refreshTokenIfExpired();
return given()
.header("Authorization", "Bearer " + adminToken)
.contentType(ContentType.JSON);
}
// Convenience method — regular user request
protected RequestSpecification asUser() {
refreshTokenIfExpired();
return given()
.header("Authorization", "Bearer " + userToken)
.contentType(ContentType.JSON);
}
// Convenience method — no auth (for negative tests)
protected RequestSpecification asGuest() {
return given()
.contentType(ContentType.JSON);
}
}import org.testng.annotations.Test;
import static org.hamcrest.Matchers.*;
public class UserApiTest extends BaseTest {
@Test
public void testAdminCanListAllUsers() {
asAdmin()
.when()
.get("/admin/users")
.then()
.statusCode(200)
.body("users.size()", greaterThan(0));
}
@Test
public void testUserCanViewOwnProfile() {
asUser()
.when()
.get("/users/me")
.then()
.statusCode(200)
.body("email", equalTo("user@example.com"));
}
@Test
public void testGuestCannotAccessProfile() {
asGuest()
.when()
.get("/users/me")
.then()
.statusCode(401);
}
@Test
public void testUserCannotAccessAdminEndpoint() {
asUser()
.when()
.get("/admin/users")
.then()
.statusCode(403);
}
}Key Point: The asAdmin(), asUser(), and asGuest() pattern is the cleanest way to handle multi-role testing. Every test method reads like English. No token management clutter in your test logic.
import io.restassured.http.ContentType;
import static io.restassured.RestAssured.*;
public class TokenRefreshHelper {
private String accessToken;
private String refreshToken;
private long expiryTime;
public TokenRefreshHelper(String email, String password) {
login(email, password);
}
private void login(String email, String password) {
var response = given()
.contentType(ContentType.JSON)
.body(String.format("""
{ "email": "%s", "password": "%s" }
""", email, password))
.when()
.post("/auth/login")
.then()
.statusCode(200)
.extract()
.jsonPath();
this.accessToken = response.getString("accessToken");
this.refreshToken = response.getString("refreshToken");
this.expiryTime = System.currentTimeMillis()
+ (response.getInt("expiresIn") * 1000L) - 60000; // 1 min buffer
}
public String getToken() {
if (System.currentTimeMillis() > expiryTime) {
refresh();
}
return accessToken;
}
private void refresh() {
var response = given()
.contentType(ContentType.JSON)
.body(String.format("""
{ "refreshToken": "%s" }
""", refreshToken))
.when()
.post("/auth/refresh")
.then()
.statusCode(200)
.extract()
.jsonPath();
this.accessToken = response.getString("accessToken");
this.expiryTime = System.currentTimeMillis()
+ (response.getInt("expiresIn") * 1000L) - 60000;
}
}Always subtract a buffer (60 seconds) from the actual expiry time. If a token expires in 3600 seconds, treat it as 3540 seconds. This prevents edge cases where the token expires between the check and the actual API call.
Q: How do you handle token management in your API test framework?
A: I use a BaseTest class with @BeforeSuite to login once and get tokens for each role (admin, user). Tokens are stored as static variables. I have helper methods like asAdmin() and asUser() that return a pre-configured RequestSpecification with the token already set. For long test runs, I check token expiry before each request and refresh automatically. This avoids calling the login endpoint before every test (wasteful) and prevents random failures from expired tokens mid-run.
Key Point: Login once in setup, reuse tokens across tests. Use helper methods like asAdmin() and asUser(). Refresh tokens automatically before they expire.