Advanced
Chapter 15 · 14 min read
Test Framework Integration
Build a reusable API testing framework with base classes, configuration management, custom helpers, logging, and reporting.
Building a Reusable API Test Framework
As your test suite grows, ad-hoc tests become unmaintainable. A well-structured framework provides reusable base classes, centralized configuration, shared helpers, and consistent reporting across all API tests.
Framework Components
- Base test class — Common setup, teardown, and shared methods
- API client wrapper — Centralized HTTP client with logging and error handling
- Configuration management — Environment-specific settings (dev, staging, prod)
- Custom helpers — Reusable methods for auth, data generation, assertions
- Logging — Request/response logging for debugging
helpers/api-client.js
// Reusable API client with logging and error handling
export class ApiClient {
constructor(baseUrl, options = {}) {
this.baseUrl = baseUrl;
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers
};
this.timeout = options.timeout || 10000;
}
async request(method, path, { body, headers, params } = {}) {
const url = new URL(path, this.baseUrl);
if (params) {
Object.entries(params).forEach(([k, v]) =>
url.searchParams.append(k, v)
);
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeout);
const startTime = Date.now();
console.log(`>> ${method} ${url}`);
try {
const resp = await fetch(url, {
method,
headers: { ...this.defaultHeaders, ...headers },
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal
});
const elapsed = Date.now() - startTime;
const data = await resp.json().catch(() => null);
console.log(`<< ${resp.status} (${elapsed}ms)`);
return { status: resp.status, data, headers: resp.headers, elapsed };
} finally {
clearTimeout(timer);
}
}
get(path, options) { return this.request('GET', path, options); }
post(path, options) { return this.request('POST', path, options); }
put(path, options) { return this.request('PUT', path, options); }
patch(path, options) { return this.request('PATCH', path, options); }
delete(path, options) { return this.request('DELETE', path, options); }
withAuth(token) {
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
return this;
}
}
// Usage
const api = new ApiClient('https://jsonplaceholder.typicode.com');
const { status, data } = await api.get('/posts/1');
console.assert(status === 200);
console.log('Title:', data.title);
BaseTest.java
package com.apitesting.base;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.filter.log.LogDetail;
import io.restassured.specification.RequestSpecification;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeSuite;
import java.io.FileInputStream;
import java.util.Properties;
public class BaseTest {
protected static RequestSpecification baseSpec;
protected static Properties config;
@BeforeSuite
public void loadConfig() throws Exception {
config = new Properties();
config.load(new FileInputStream("src/test/resources/config.properties"));
}
@BeforeClass
public void setup() {
String env = System.getProperty("env", "test");
String baseUrl = config.getProperty(env + ".base.url",
"https://jsonplaceholder.typicode.com");
baseSpec = new RequestSpecBuilder()
.setBaseUri(baseUrl)
.setContentType("application/json")
.setAccept("application/json")
.log(LogDetail.ALL) // Log all requests
.build();
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
protected RequestSpecification authSpec(String token) {
return given()
.spec(baseSpec)
.header("Authorization", "Bearer " + token);
}
protected RequestSpecification spec() {
return given().spec(baseSpec);
}
}
// Usage in test class:
// public class UserTests extends BaseTest {
// @Test
// public void testGetUser() {
// spec().when().get("/users/1")
// .then().statusCode(200);
// }
// }
helpers/api_client.py
"""Reusable API client with logging and error handling"""
import requests
import logging
import time
logger = logging.getLogger(__name__)
class ApiClient:
def __init__(self, base_url, headers=None, timeout=10):
self.session = requests.Session()
self.base_url = base_url
self.session.headers.update({
'Content-Type': 'application/json',
'Accept': 'application/json',
**(headers or {})
})
self.timeout = timeout
def request(self, method, path, **kwargs):
url = f'{self.base_url}{path}'
kwargs.setdefault('timeout', self.timeout)
logger.info(f'>> {method} {url}')
start = time.time()
response = self.session.request(method, url, **kwargs)
elapsed = (time.time() - start) * 1000
logger.info(f'<< {response.status_code} ({elapsed:.0f}ms)')
return response
def get(self, path, **kwargs):
return self.request('GET', path, **kwargs)
def post(self, path, **kwargs):
return self.request('POST', path, **kwargs)
def put(self, path, **kwargs):
return self.request('PUT', path, **kwargs)
def patch(self, path, **kwargs):
return self.request('PATCH', path, **kwargs)
def delete(self, path, **kwargs):
return self.request('DELETE', path, **kwargs)
def with_auth(self, token):
self.session.headers['Authorization'] = f'Bearer {token}'
return self
# conftest.py fixture
# import pytest
# @pytest.fixture
# def api():
# return ApiClient('https://jsonplaceholder.typicode.com')
conftest.py
"""Shared pytest fixtures"""
import pytest
import os
from helpers.api_client import ApiClient
@pytest.fixture(scope='session')
def api():
"""Session-scoped API client"""
base_url = os.getenv('API_BASE_URL',
'https://jsonplaceholder.typicode.com')
return ApiClient(base_url)
@pytest.fixture(scope='session')
def auth_api():
"""Authenticated API client"""
client = ApiClient('https://jsonplaceholder.typicode.com')
token = os.getenv('API_TOKEN', 'test-token')
return client.with_auth(token)
@pytest.fixture
def sample_post():
return {'title': 'Test Post', 'body': 'Test Body', 'userId': 1}
API Testing
Advanced
Test Framework Integration
Written by PV
© 2026 All Rights Reserved