API Framework Design
Building scalable API automation framework
Project Structure
A well-organized project structure ensures maintainability and scalability.
api-automation-framework/
├── src/
│ ├── main/java/
│ │ └── com/framework/
│ │ ├── config/
│ │ │ ├── ConfigReader.java
│ │ │ └── Environment.java
│ │ ├── constants/
│ │ │ └── Endpoints.java
│ │ ├── models/
│ │ │ ├── request/
│ │ │ │ └── UserRequest.java
│ │ │ └── response/
│ │ │ └── UserResponse.java
│ │ ├── specs/
│ │ │ └── RequestSpecs.java
│ │ └── utils/
│ │ ├── AuthManager.java
│ │ ├── DataGenerator.java
│ │ └── JsonUtils.java
│ └── test/
│ ├── java/
│ │ └── com/tests/
│ │ ├── base/
│ │ │ └── BaseTest.java
│ │ ├── users/
│ │ │ └── UserApiTests.java
│ │ └── auth/
│ │ └── AuthTests.java
│ └── resources/
│ ├── config/
│ │ ├── config.properties
│ │ ├── qa.properties
│ │ └── prod.properties
│ ├── schemas/
│ │ └── user-schema.json
│ └── testdata/
│ └── users.json
├── pom.xml
└── testng.xml
Configuration Management
Properties File
Create src/test/resources/config/config.properties:
# Base Configuration
base.uri=https://reqres.in
base.path=/api
# Timeouts
connection.timeout=10000
socket.timeout=10000
# Logging
log.request=true
log.response=true
# Auth
auth.username=testuser
auth.password=testpass
Environment-specific Config
qa.properties:
base.uri=https://qa-api.example.com
auth.username=qa_user
ConfigReader Class
public class ConfigReader {
private static final Properties properties = new Properties();
private static final String ENV = System.getProperty("env", "config");
static {
loadProperties();
}
private static void loadProperties() {
try {
// Load default config
String defaultPath = "config/config.properties";
InputStream defaultStream = ConfigReader.class.getClassLoader()
.getResourceAsStream(defaultPath);
properties.load(defaultStream);
// Override with environment-specific config
if (!ENV.equals("config")) {
String envPath = "config/" + ENV + ".properties";
InputStream envStream = ConfigReader.class.getClassLoader()
.getResourceAsStream(envPath);
if (envStream != null) {
properties.load(envStream);
}
}
} catch (IOException e) {
throw new RuntimeException("Failed to load config", e);
}
}
public static String get(String key) {
return properties.getProperty(key);
}
public static String getBaseUri() {
return get("base.uri");
}
public static String getBasePath() {
return get("base.path");
}
public static int getConnectionTimeout() {
return Integer.parseInt(get("connection.timeout"));
}
public static boolean logRequest() {
return Boolean.parseBoolean(get("log.request"));
}
}
Running with Different Environments
# Run with QA config
mvn test -Denv=qa
# Run with Production config
mvn test -Denv=prod
Endpoint Constants
Endpoints Class
public final class Endpoints {
private Endpoints() {} // Prevent instantiation
// User endpoints
public static final String USERS = "/users";
public static final String USER_BY_ID = "/users/{id}";
public static final String USER_AVATAR = "/users/{id}/avatar";
// Auth endpoints
public static final String LOGIN = "/login";
public static final String REGISTER = "/register";
public static final String LOGOUT = "/logout";
// Resource endpoints
public static final String RESOURCES = "/unknown";
public static final String RESOURCE_BY_ID = "/unknown/{id}";
}
HTTP Status Codes
public final class StatusCodes {
private StatusCodes() {}
public static final int OK = 200;
public static final int CREATED = 201;
public static final int NO_CONTENT = 204;
public static final int BAD_REQUEST = 400;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int SERVER_ERROR = 500;
}
POJO Models
Request Models
// CreateUserRequest.java
public class CreateUserRequest {
private String name;
private String job;
// Builder pattern for clean instantiation
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final CreateUserRequest request = new CreateUserRequest();
public Builder name(String name) {
request.name = name;
return this;
}
public Builder job(String job) {
request.job = job;
return this;
}
public CreateUserRequest build() {
return request;
}
}
// Getters
public String getName() { return name; }
public String getJob() { return job; }
}
Response Models
// UserResponse.java
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserResponse {
private UserData data;
private Support support;
@JsonIgnoreProperties(ignoreUnknown = true)
public static class UserData {
private int id;
private String email;
@JsonProperty("first_name")
private String firstName;
@JsonProperty("last_name")
private String lastName;
private String avatar;
// Getters and setters
public int getId() { return id; }
public String getEmail() { return email; }
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public String getAvatar() { return avatar; }
}
public static class Support {
private String url;
private String text;
public String getUrl() { return url; }
public String getText() { return text; }
}
public UserData getData() { return data; }
public Support getSupport() { return support; }
}
Generic List Response
// ListResponse.java - Handles paginated responses
@JsonIgnoreProperties(ignoreUnknown = true)
public class ListResponse<T> {
private int page;
@JsonProperty("per_page")
private int perPage;
private int total;
@JsonProperty("total_pages")
private int totalPages;
private List<T> data;
// Getters
public int getPage() { return page; }
public int getPerPage() { return perPage; }
public int getTotal() { return total; }
public int getTotalPages() { return totalPages; }
public List<T> getData() { return data; }
}
Request Specifications
RequestSpecs Factory
public class RequestSpecs {
public static RequestSpecification getDefaultSpec() {
RequestSpecBuilder builder = new RequestSpecBuilder()
.setBaseUri(ConfigReader.getBaseUri())
.setBasePath(ConfigReader.getBasePath())
.setContentType(ContentType.JSON)
.setAccept(ContentType.JSON);
if (ConfigReader.logRequest()) {
builder.log(LogDetail.ALL);
}
return builder.build();
}
public static RequestSpecification getAuthenticatedSpec(String token) {
return new RequestSpecBuilder()
.addRequestSpecification(getDefaultSpec())
.addHeader("Authorization", "Bearer " + token)
.build();
}
public static RequestSpecification getFormSpec() {
return new RequestSpecBuilder()
.setBaseUri(ConfigReader.getBaseUri())
.setBasePath(ConfigReader.getBasePath())
.setContentType(ContentType.URLENC)
.build();
}
public static RequestSpecification getMultipartSpec() {
return new RequestSpecBuilder()
.setBaseUri(ConfigReader.getBaseUri())
.setBasePath(ConfigReader.getBasePath())
.setContentType("multipart/form-data")
.build();
}
}
Response Specifications
public class ResponseSpecs {
public static ResponseSpecification successSpec() {
return new ResponseSpecBuilder()
.expectStatusCode(StatusCodes.OK)
.expectContentType(ContentType.JSON)
.build();
}
public static ResponseSpecification createdSpec() {
return new ResponseSpecBuilder()
.expectStatusCode(StatusCodes.CREATED)
.expectContentType(ContentType.JSON)
.build();
}
public static ResponseSpecification notFoundSpec() {
return new ResponseSpecBuilder()
.expectStatusCode(StatusCodes.NOT_FOUND)
.build();
}
public static ResponseSpecification unauthorizedSpec() {
return new ResponseSpecBuilder()
.expectStatusCode(StatusCodes.UNAUTHORIZED)
.build();
}
}
Base Test Class
public class BaseTest {
protected RequestSpecification requestSpec;
protected String authToken;
@BeforeClass(alwaysRun = true)
public void globalSetup() {
// Configure Rest Assured defaults
RestAssured.baseURI = ConfigReader.getBaseUri();
RestAssured.basePath = ConfigReader.getBasePath();
// Setup default request spec
requestSpec = RequestSpecs.getDefaultSpec();
// Enable logging on validation failure
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@BeforeMethod
public void testSetup(Method method) {
System.out.println("Starting: " + method.getName());
}
@AfterMethod
public void testTeardown(ITestResult result) {
if (result.getStatus() == ITestResult.FAILURE) {
System.out.println("Test Failed: " + result.getName());
}
}
@AfterClass(alwaysRun = true)
public void globalTeardown() {
RestAssured.reset();
}
// Helper methods
protected Response sendGet(String endpoint) {
return given()
.spec(requestSpec)
.when()
.get(endpoint);
}
protected Response sendPost(String endpoint, Object body) {
return given()
.spec(requestSpec)
.body(body)
.when()
.post(endpoint);
}
protected Response sendPut(String endpoint, Object body) {
return given()
.spec(requestSpec)
.body(body)
.when()
.put(endpoint);
}
protected Response sendDelete(String endpoint) {
return given()
.spec(requestSpec)
.when()
.delete(endpoint);
}
}
Utility Classes
AuthManager
public class AuthManager {
private static String cachedToken;
private static long tokenExpiry;
public static String getToken() {
if (cachedToken == null || isTokenExpired()) {
cachedToken = fetchNewToken();
tokenExpiry = System.currentTimeMillis() + 3600000; // 1 hour
}
return cachedToken;
}
private static boolean isTokenExpired() {
return System.currentTimeMillis() >= tokenExpiry - 60000;
}
private static String fetchNewToken() {
return given()
.spec(RequestSpecs.getFormSpec())
.formParam("email", ConfigReader.get("auth.username"))
.formParam("password", ConfigReader.get("auth.password"))
.when()
.post(Endpoints.LOGIN)
.then()
.statusCode(StatusCodes.OK)
.extract().path("token");
}
public static void clearToken() {
cachedToken = null;
tokenExpiry = 0;
}
}
DataGenerator
public class DataGenerator {
private static final Faker faker = new Faker();
public static CreateUserRequest randomUser() {
return CreateUserRequest.builder()
.name(faker.name().fullName())
.job(faker.job().title())
.build();
}
public static String randomEmail() {
return faker.internet().emailAddress();
}
public static String randomName() {
return faker.name().fullName();
}
public static Map<String, Object> randomUserMap() {
Map<String, Object> user = new HashMap<>();
user.put("name", randomName());
user.put("job", faker.job().title());
return user;
}
}
JsonUtils
public class JsonUtils {
private static final ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
public static <T> T fromJson(String json, Class<T> clazz) {
try {
return mapper.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to parse JSON", e);
}
}
public static String toJson(Object object) {
try {
return mapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to convert to JSON", e);
}
}
public static String readJsonFile(String filePath) {
try {
return new String(Files.readAllBytes(Paths.get(filePath)));
} catch (IOException e) {
throw new RuntimeException("Failed to read file: " + filePath, e);
}
}
public static <T> T readJsonFileAs(String filePath, Class<T> clazz) {
String json = readJsonFile(filePath);
return fromJson(json, clazz);
}
}
Complete Test Examples
User API Tests
public class UserApiTests extends BaseTest {
@Test(description = "Verify user creation")
public void testCreateUser() {
CreateUserRequest user = DataGenerator.randomUser();
sendPost(Endpoints.USERS, user)
.then()
.spec(ResponseSpecs.createdSpec())
.body("name", equalTo(user.getName()))
.body("job", equalTo(user.getJob()))
.body("id", notNullValue())
.body("createdAt", notNullValue());
}
@Test(description = "Verify get single user")
public void testGetUser() {
int userId = 2;
UserResponse response = given()
.spec(requestSpec)
.pathParam("id", userId)
.when()
.get(Endpoints.USER_BY_ID)
.then()
.spec(ResponseSpecs.successSpec())
.extract().as(UserResponse.class);
Assert.assertEquals(response.getData().getId(), userId);
Assert.assertNotNull(response.getData().getEmail());
}
@Test(description = "Verify user list pagination")
public void testGetUsersList() {
given()
.spec(requestSpec)
.queryParam("page", 1)
.queryParam("per_page", 5)
.when()
.get(Endpoints.USERS)
.then()
.spec(ResponseSpecs.successSpec())
.body("page", equalTo(1))
.body("per_page", equalTo(5))
.body("data.size()", lessThanOrEqualTo(5))
.body("data.id", everyItem(greaterThan(0)));
}
@Test(description = "Verify update user")
public void testUpdateUser() {
CreateUserRequest updatedUser = CreateUserRequest.builder()
.name("Updated Name")
.job("Senior Engineer")
.build();
given()
.spec(requestSpec)
.pathParam("id", 2)
.body(updatedUser)
.when()
.put(Endpoints.USER_BY_ID)
.then()
.spec(ResponseSpecs.successSpec())
.body("name", equalTo("Updated Name"))
.body("updatedAt", notNullValue());
}
@Test(description = "Verify delete user")
public void testDeleteUser() {
given()
.spec(requestSpec)
.pathParam("id", 2)
.when()
.delete(Endpoints.USER_BY_ID)
.then()
.statusCode(StatusCodes.NO_CONTENT);
}
@Test(description = "Verify user not found")
public void testUserNotFound() {
given()
.spec(requestSpec)
.pathParam("id", 9999)
.when()
.get(Endpoints.USER_BY_ID)
.then()
.spec(ResponseSpecs.notFoundSpec());
}
}
Reporting Integration
Extent Reports Setup
public class ExtentReportListener implements ITestListener {
private static ExtentReports extent;
private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();
@Override
public void onStart(ITestContext context) {
extent = new ExtentReports();
ExtentSparkReporter spark = new ExtentSparkReporter("reports/extent-report.html");
spark.config().setTheme(Theme.DARK);
spark.config().setDocumentTitle("API Test Report");
spark.config().setReportName("Rest Assured Tests");
extent.attachReporter(spark);
}
@Override
public void onTestStart(ITestResult result) {
ExtentTest extentTest = extent.createTest(result.getMethod().getMethodName(),
result.getMethod().getDescription());
test.set(extentTest);
}
@Override
public void onTestSuccess(ITestResult result) {
test.get().pass("Test passed");
}
@Override
public void onTestFailure(ITestResult result) {
test.get().fail(result.getThrowable());
}
@Override
public void onFinish(ITestContext context) {
extent.flush();
}
}
Allure Reports
Add dependency and use annotations:
@Epic("User Management")
@Feature("User API")
public class UserApiTests extends BaseTest {
@Test
@Story("Create User")
@Description("Verify user can be created successfully")
@Severity(SeverityLevel.CRITICAL)
public void testCreateUser() {
CreateUserRequest user = DataGenerator.randomUser();
Response response = sendPost(Endpoints.USERS, user);
Allure.step("Verify status code is 201", () -> {
response.then().statusCode(StatusCodes.CREATED);
});
Allure.step("Verify response contains user name", () -> {
response.then().body("name", equalTo(user.getName()));
});
}
}
CI/CD Integration
Maven POM Configuration
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>testng.xml</suiteXmlFile>
</suiteXmlFiles>
<systemPropertyVariables>
<env>${env}</env>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
GitHub Actions Workflow
name: API Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 6 * * *' # Daily at 6 AM
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Run API Tests
run: mvn test -Denv=qa
env:
API_KEY: ${{ secrets.API_KEY }}
- name: Upload Test Report
uses: actions/upload-artifact@v4
if: always()
with:
name: test-report
path: target/surefire-reports/
- name: Publish Allure Report
uses: simple-elf/allure-report-action@v1.7
if: always()
with:
allure_results: target/allure-results
Jenkins Pipeline
pipeline {
agent any
parameters {
choice(name: 'ENV', choices: ['qa', 'staging', 'prod'], description: 'Environment')
}
stages {
stage('Checkout') {
steps {
git 'https://github.com/your-repo/api-tests.git'
}
}
stage('Run Tests') {
steps {
sh "mvn clean test -Denv=${params.ENV}"
}
}
stage('Publish Report') {
steps {
allure includeProperties: false,
jdk: '',
results: [[path: 'target/allure-results']]
}
}
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
failure {
mail to: 'team@example.com',
subject: "API Tests Failed - ${env.JOB_NAME}",
body: "Check: ${env.BUILD_URL}"
}
}
}
Best Practices
Test Data Management
// Use @DataProvider for data-driven tests
@DataProvider(name = "validUsers")
public Object[][] validUsers() {
return new Object[][] {
{"John", "Engineer"},
{"Jane", "Manager"},
{"Bob", "Developer"}
};
}
@Test(dataProvider = "validUsers")
public void testCreateMultipleUsers(String name, String job) {
CreateUserRequest user = CreateUserRequest.builder()
.name(name)
.job(job)
.build();
sendPost(Endpoints.USERS, user)
.then()
.statusCode(StatusCodes.CREATED)
.body("name", equalTo(name));
}
Error Handling
// Custom assertions for common validations
public class ApiAssertions {
public static void assertSuccessResponse(Response response) {
int statusCode = response.getStatusCode();
Assert.assertTrue(statusCode >= 200 && statusCode