Hands-On: Building Your First API Automation Framework
Step-by-step guide to building a production-ready REST API test automation framework with Java, RestAssured, and TestNG
What You'll Build
A complete API test automation framework for an e-commerce platform with:
- ✅ Reusable REST API client
- ✅ Test data management
- ✅ Configurable environments
- ✅ Comprehensive assertions
- ✅ Test reporting
- ✅ CI/CD integration
Time to complete: 2-3 hours
Skill level: Basic Java knowledge required
Prerequisites
# Required installations
- Java 11 or higher
- Maven 3.6+
- IDE (IntelliJ IDEA recommended)
- GitStep 1: Project Setup
Create Maven Project
# Create project directory
mkdir api-automation-framework
cd api-automation-framework
# Initialize Maven project
mvn archetype:generate \
-DgroupId=com.company.qe \
-DartifactId=api-tests \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
cd api-testsConfigure pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.company.qe</groupId>
<artifactId>api-tests</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<restassured.version>5.3.2</restassured.version>
<testng.version>7.8.0</testng.version>
<jackson.version>2.15.3</jackson.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<!-- REST Assured for API testing -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${restassured.version}</version>
</dependency>
<!-- TestNG for test execution -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
</dependency>
<!-- Jackson for JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Lombok to reduce boilerplate -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- AssertJ for fluent assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
</dependency>
<!-- SLF4J for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>testng.xml</suiteXmlFile>
</suiteXmlFiles>
</configuration>
</plugin>
</plugins>
</build>
</project>Step 2: Create Project Structure
# Create directory structure
mkdir -p src/test/java/com/company/qe/{api,models,config,utils,tests}
mkdir -p src/test/resourcesResulting structure:
api-tests/
├── pom.xml
└── src/test/
├── java/com/company/qe/
│ ├── api/ # API clients
│ ├── models/ # Request/Response POJOs
│ ├── config/ # Configuration
│ ├── utils/ # Helper utilities
│ └── tests/ # Test classes
└── resources/
├── config.properties
└── testng.xmlStep 3: Configuration Management
Create config.properties
# src/test/resources/config.properties
# Environment
env=qa
# QA Environment
qa.base.url=https://api-qa.example.com
qa.api.version=v1
# Staging Environment
staging.base.url=https://api-staging.example.com
staging.api.version=v1
# Test User Credentials
test.user.email=test@example.com
test.user.password=Test123!
# API Timeouts
api.timeout.connect=5000
api.timeout.read=10000Create ConfigManager
// src/test/java/com/company/qe/config/ConfigManager.java
package com.company.qe.config;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class ConfigManager {
private static Properties properties;
private static String environment;
static {
loadProperties();
}
private static void loadProperties() {
properties = new Properties();
try {
String configFile = "src/test/resources/config.properties";
properties.load(new FileInputStream(configFile));
environment = System.getProperty("env", properties.getProperty("env", "qa"));
} catch (IOException e) {
throw new RuntimeException("Failed to load configuration", e);
}
}
public static String getBaseUrl() {
return properties.getProperty(environment + ".base.url");
}
public static String getApiVersion() {
return properties.getProperty(environment + ".api.version");
}
public static String getProperty(String key) {
return properties.getProperty(key);
}
public static String getEnvironment() {
return environment;
}
}Step 4: Create API Models
Product Model
// src/test/java/com/company/qe/models/Product.java
package com.company.qe.models;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Product {
private String id;
private String sku;
private String name;
private String description;
private Double price;
private Integer stock;
private String category;
private Boolean inStock;
}Order Model
// src/test/java/com/company/qe/models/Order.java
package com.company.qe.models;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Order {
private String id;
private String userId;
private List<OrderItem> items;
private Double subtotal;
private Double tax;
private Double total;
private String status;
private String createdAt;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
class OrderItem {
private String productId;
private String sku;
private Integer quantity;
private Double price;
}User Model
// src/test/java/com/company/qe/models/User.java
package com.company.qe.models;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
private String id;
private String email;
private String firstName;
private String lastName;
private String token;
}Step 5: Build API Clients
Base API Client
// src/test/java/com/company/qe/api/BaseApiClient.java
package com.company.qe.api;
import com.company.qe.config.ConfigManager;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.filter.log.RequestLoggingFilter;
import io.restassured.filter.log.ResponseLoggingFilter;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
public class BaseApiClient {
protected RequestSpecification requestSpec;
public BaseApiClient() {
this.requestSpec = new RequestSpecBuilder()
.setBaseUri(ConfigManager.getBaseUrl())
.setBasePath("/api/" + ConfigManager.getApiVersion())
.setContentType(ContentType.JSON)
.addFilter(new RequestLoggingFilter())
.addFilter(new ResponseLoggingFilter())
.build();
}
protected RequestSpecification withAuth(String token) {
return RestAssured.given(requestSpec)
.header("Authorization", "Bearer " + token);
}
}Product API Client
// src/test/java/com/company/qe/api/ProductApiClient.java
package com.company.qe.api;
import com.company.qe.models.Product;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import java.util.List;
public class ProductApiClient extends BaseApiClient {
public Response getProducts() {
return RestAssured.given(requestSpec)
.when()
.get("/products");
}
public Response getProductById(String productId) {
return RestAssured.given(requestSpec)
.pathParam("id", productId)
.when()
.get("/products/{id}");
}
public Response searchProducts(String query) {
return RestAssured.given(requestSpec)
.queryParam("q", query)
.when()
.get("/products/search");
}
public Response createProduct(Product product, String authToken) {
return withAuth(authToken)
.body(product)
.when()
.post("/products");
}
public Response updateProduct(String productId, Product product, String authToken) {
return withAuth(authToken)
.pathParam("id", productId)
.body(product)
.when()
.put("/products/{id}");
}
public Response deleteProduct(String productId, String authToken) {
return withAuth(authToken)
.pathParam("id", productId)
.when()
.delete("/products/{id}");
}
// Helper method to convert response to Product list
public List<Product> getProductsAsList(Response response) {
return response.jsonPath().getList(".", Product.class);
}
// Helper method to convert response to single Product
public Product getProductFromResponse(Response response) {
return response.as(Product.class);
}
}Order API Client
// src/test/java/com/company/qe/api/OrderApiClient.java
package com.company.qe.api;
import com.company.qe.models.Order;
import io.restassured.response.Response;
public class OrderApiClient extends BaseApiClient {
public Response createOrder(Order order, String authToken) {
return withAuth(authToken)
.body(order)
.when()
.post("/orders");
}
public Response getOrderById(String orderId, String authToken) {
return withAuth(authToken)
.pathParam("id", orderId)
.when()
.get("/orders/{id}");
}
public Response getUserOrders(String userId, String authToken) {
return withAuth(authToken)
.queryParam("userId", userId)
.when()
.get("/orders");
}
public Response updateOrderStatus(String orderId, String status, String authToken) {
return withAuth(authToken)
.pathParam("id", orderId)
.body("{\"status\": \"" + status + "\"}")
.when()
.patch("/orders/{id}/status");
}
public Order getOrderFromResponse(Response response) {
return response.as(Order.class);
}
}Step 6: Create Test Data Utilities
// src/test/java/com/company/qe/utils/TestDataGenerator.java
package com.company.qe.utils;
import com.company.qe.models.Product;
import com.company.qe.models.Order;
import com.company.qe.models.OrderItem;
import java.util.List;
import java.util.UUID;
public class TestDataGenerator {
public static Product createTestProduct() {
return Product.builder()
.sku("TEST-SKU-" + UUID.randomUUID().toString().substring(0, 8))
.name("Test Product " + System.currentTimeMillis())
.description("This is a test product")
.price(99.99)
.stock(100)
.category("Electronics")
.inStock(true)
.build();
}
public static Order createTestOrder(String userId, String productId) {
OrderItem item = OrderItem.builder()
.productId(productId)
.sku("TEST-SKU-001")
.quantity(2)
.price(99.99)
.build();
return Order.builder()
.userId(userId)
.items(List.of(item))
.subtotal(199.98)
.tax(16.00)
.total(215.98)
.status("pending")
.build();
}
public static String generateRandomEmail() {
return "test" + System.currentTimeMillis() + "@example.com";
}
}Step 7: Write Your First Tests
Product API Tests
// src/test/java/com/company/qe/tests/ProductApiTest.java
package com.company.qe.tests;
import com.company.qe.api.ProductApiClient;
import com.company.qe.models.Product;
import com.company.qe.utils.TestDataGenerator;
import io.restassured.response.Response;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.*;
public class ProductApiTest {
private ProductApiClient productApi;
@BeforeClass
public void setup() {
productApi = new ProductApiClient();
}
@Test(description = "Get all products successfully")
public void testGetAllProducts() {
Response response = productApi.getProducts();
// Verify status code
assertThat(response.statusCode()).isEqualTo(200);
// Verify response has products
List<Product> products = productApi.getProductsAsList(response);
assertThat(products).isNotEmpty();
// Verify first product has required fields
Product firstProduct = products.get(0);
assertThat(firstProduct.getId()).isNotNull();
assertThat(firstProduct.getName()).isNotNull();
assertThat(firstProduct.getPrice()).isGreaterThan(0);
}
@Test(description = "Search products by keyword")
public void testSearchProducts() {
String searchQuery = "laptop";
Response response = productApi.searchProducts(searchQuery);
response.then()
.statusCode(200)
.body("size()", greaterThan(0))
.body("[0].name", containsStringIgnoringCase(searchQuery));
}
@Test(description = "Get product by ID")
public void testGetProductById() {
// First, get all products to get a valid ID
Response allProducts = productApi.getProducts();
List<Product> products = productApi.getProductsAsList(allProducts);
String productId = products.get(0).getId();
// Get specific product
Response response = productApi.getProductById(productId);
Product product = productApi.getProductFromResponse(response);
assertThat(response.statusCode()).isEqualTo(200);
assertThat(product.getId()).isEqualTo(productId);
assertThat(product.getName()).isNotNull();
}
@Test(description = "Get non-existent product returns 404")
public void testGetNonExistentProduct() {
String fakeId = "non-existent-id-12345";
Response response = productApi.getProductById(fakeId);
assertThat(response.statusCode()).isEqualTo(404);
}
@Test(description = "Search with empty query returns all products")
public void testSearchWithEmptyQuery() {
Response response = productApi.searchProducts("");
assertThat(response.statusCode()).isEqualTo(200);
assertThat(productApi.getProductsAsList(response)).isNotEmpty();
}
}Order API Tests
// src/test/java/com/company/qe/tests/OrderApiTest.java
package com.company.qe.tests;
import com.company.qe.api.OrderApiClient;
import com.company.qe.models.Order;
import com.company.qe.utils.TestDataGenerator;
import io.restassured.response.Response;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class OrderApiTest {
private OrderApiClient orderApi;
private String authToken = "test-token-123"; // In real scenario, get from login
@BeforeClass
public void setup() {
orderApi = new OrderApiClient();
}
@Test(description = "Create order successfully")
public void testCreateOrder() {
// Arrange
Order testOrder = TestDataGenerator.createTestOrder("user-123", "product-456");
// Act
Response response = orderApi.createOrder(testOrder, authToken);
Order createdOrder = orderApi.getOrderFromResponse(response);
// Assert
assertThat(response.statusCode()).isEqualTo(201);
assertThat(createdOrder.getId()).isNotNull();
assertThat(createdOrder.getStatus()).isEqualTo("pending");
assertThat(createdOrder.getTotal()).isEqualTo(testOrder.getTotal());
}
@Test(description = "Get order by ID")
public void testGetOrderById() {
// Create order first
Order testOrder = TestDataGenerator.createTestOrder("user-123", "product-456");
Response createResponse = orderApi.createOrder(testOrder, authToken);
String orderId = orderApi.getOrderFromResponse(createResponse).getId();
// Get the order
Response response = orderApi.getOrderById(orderId, authToken);
Order retrievedOrder = orderApi.getOrderFromResponse(response);
assertThat(response.statusCode()).isEqualTo(200);
assertThat(retrievedOrder.getId()).isEqualTo(orderId);
}
@Test(description = "Update order status")
public void testUpdateOrderStatus() {
// Create order
Order testOrder = TestDataGenerator.createTestOrder("user-123", "product-456");
Response createResponse = orderApi.createOrder(testOrder, authToken);
String orderId = orderApi.getOrderFromResponse(createResponse).getId();
// Update status
Response updateResponse = orderApi.updateOrderStatus(orderId, "shipped", authToken);
assertThat(updateResponse.statusCode()).isEqualTo(200);
assertThat(updateResponse.path("status").toString()).isEqualTo("shipped");
}
}Step 8: Configure TestNG
<!-- testng.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="API Test Suite" parallel="methods" thread-count="3">
<test name="Product API Tests">
<classes>
<class name="com.company.qe.tests.ProductApiTest"/>
</classes>
</test>
<test name="Order API Tests">
<classes>
<class name="com.company.qe.tests.OrderApiTest"/>
</classes>
</test>
</suite>Step 9: Run Tests
# Run all tests
mvn clean test
# Run specific test class
mvn test -Dtest=ProductApiTest
# Run with specific environment
mvn test -Denv=staging
# Run with parallel execution
mvn test -DthreadCount=5Step 10: CI/CD Integration
GitHub Actions
# .github/workflows/api-tests.yml
name: API Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 */6 * * *' # Every 6 hours
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- name: Run tests
run: mvn clean test -Denv=qa
- name: Publish test report
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: target/surefire-reports/Best Practices Implemented
✅ Separation of Concerns: API clients, models, and tests are separate
✅ Reusability: Base API client for common functionality
✅ Configuration Management: Environment-specific config
✅ Test Data Management: Utilities for generating test data
✅ Clean Assertions: Using AssertJ for fluent assertions
✅ Logging: Request/response logging for debugging
✅ Parallel Execution: Tests run in parallel for speed
✅ CI/CD Ready: GitHub Actions integration
Next Steps
- Add more test scenarios: Negative tests, edge cases
- Implement authentication: Real login flow
- Add test data cleanup: AfterMethod hooks
- Create test reports: Extent Reports or Allure
- Add performance tests: Measure response times
- Contract testing: Add Pact for API contracts
- Mock external services: Use WireMock
Conclusion
You now have a production-ready API automation framework! This framework follows industry best practices and can scale to hundreds of tests. Start adding tests for your platform's APIs and watch your test coverage grow.
Remember: Good test automation is maintainable, reliable, and provides fast feedback. Keep your tests focused, independent, and well-organized.
Happy testing! 🚀
Comments (0)
Loading comments...