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