Build a Modern Python HTTPX API Testing Framework with Async Support

Learn how to create a modular Python API testing framework using httpx, pytest, and Allure, featuring multi‑environment configuration, synchronous and asynchronous requests, data‑driven and mock testing, comprehensive logging, and automated report generation, plus optional extensions for CI/CD, token authentication, and coverage analysis.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
Build a Modern Python HTTPX API Testing Framework with Async Support

Framework Features

Multi‑environment configuration management (dev/test/prod)

Interface encapsulation and modular design

Logging

Data‑driven testing with pytest parametrization

Allure report support

Mock testing using respx

Asynchronous request support (async/await)

Automated test execution entry script

Test report generation

Project Structure

httpx_api_test_framework/
├── config/                     # configuration files
│   ├── __init__.py
│   ├── settings.py            # environment settings
│   └── environments.json      # multi‑environment config
├── utils/                     # utility functions
│   ├── __init__.py
│   ├── http_client.py        # sync/async httpx client wrapper
│   ├── logger.py             # logging utility
│   └── data_loader.py        # test data loader (CSV/JSON)
├── api/                       # API wrapper modules
│   ├── __init__.py
│   ├── base_api.py           # base API class
│   ├── user_api.py           # user‑related endpoints
│   └── auth_api.py           # authentication endpoints
├── testcases/                 # test cases
│   ├── __init__.py
│   ├── test_user_api.py      # user API tests with parametrization
│   └── test_mock_api.py      # mock tests using respx
├── reports/                   # test report output
│   └── allure/               # Allure report directory
├── conftest.py                # pytest global fixtures
├── run_tests.py               # test execution entry script
├── requirements.txt           # project dependencies
└── README.md                  # project documentation

Installation

pip install httpx pytest pytest-html allure-pytest respx pandas pytest-xdist

Save the following as requirements.txt:

httpx>=0.27.0
pytest>=8.1.1
pytest-html>=3.2.0
allure-pytest>=2.0.0
respx>=0.24.1
pandas>=2.0.0
pytest-xdist>=3.5.0

Configuration Files

config/environments.json

{
  "dev": {
    "base_url": "https://jsonplaceholder.typicode.com",
    "headers": {"Content-Type": "application/json"},
    "timeout": 10
  },
  "test": {
    "base_url": "https://jsonplaceholder.typicode.com",
    "headers": {"Content-Type": "application/json"},
    "timeout": 10
  }
}

config/settings.py

# config/settings.py
import os
import json
ENV = os.getenv("API_ENV", "dev")  # default to dev
with open(os.path.join(os.path.dirname(__file__), "environments.json")) as f:
    env_config = json.load(f)
BASE_URL = env_config[ENV]["base_url"]
HEADERS = env_config[ENV]["headers"]
TIMEOUT = env_config[ENV]["timeout"]

Core Code Explanation

HTTP Client Wrapper (utils/http_client.py)

# utils/http_client.py
import httpx
from config.settings import BASE_URL, HEADERS, TIMEOUT

class HttpClient:
    def __init__(self, base_url=BASE_URL):
        self.base_url = base_url
        self.headers = HEADERS

    def get(self, endpoint: str, params: dict = None):
        url = f"{self.base_url}{endpoint}"
        return httpx.get(url, headers=self.headers, params=params, timeout=TIMEOUT)

    def post(self, endpoint: str, json_data: dict = None):
        url = f"{self.base_url}{endpoint}"
        return httpx.post(url, headers=self.headers, json=json_data, timeout=TIMEOUT)

    async def async_get(self, endpoint: str):
        async with httpx.AsyncClient(base_url=self.base_url, headers=self.headers) as client:
            response = await client.get(endpoint, timeout=TIMEOUT)
            return response

    async def async_post(self, endpoint: str, json_data: dict):
        async with httpx.AsyncClient(base_url=self.base_url, headers=self.headers) as client:
            response = await client.post(endpoint, json=json_data, timeout=TIMEOUT)
            return response

Logging Utility (utils/logger.py)

# utils/logger.py
import logging
import os

LOG_DIR = os.path.join(os.getcwd(), "logs")
if not os.path.exists(LOG_DIR):
    os.makedirs(LOG_DIR)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(filename)s - %(message)s",
    filename=os.path.join(LOG_DIR, "test.log")
)
logger = logging.getLogger(__name__)

Data Loader (utils/data_loader.py)

# utils/data_loader.py
import pandas as pd

def load_test_data(file_path):
    if file_path.endswith(".csv"):
        return pd.read_csv(file_path).to_dict(orient="records")
    elif file_path.endswith(".json"):
        return pd.read_json(file_path).to_dict(orient="records")
    else:
        raise ValueError("Unsupported file format.")

Base API (api/base_api.py)

# api/base_api.py
from utils.http_client import HttpClient
client = HttpClient()

User API (api/user_api.py)

# api/user_api.py
from api.base_api import client

def get_users():
    return client.get("/users")

def get_user_by_id(user_id):
    return client.get(f"/users/{user_id}")

Test Cases

testcases/test_user_api.py

# testcases/test_user_api.py
import pytest
from api.user_api import get_users, get_user_by_id
from utils.data_loader import load_test_data
import os

# Load CSV test data
data_file = os.path.join(os.getcwd(), "test_data", "users.csv")
test_data = load_test_data(data_file)

@pytest.mark.parametrize("user_id", [1, 2, 3])
def test_get_user_by_id(user_id):
    response = get_user_by_id(user_id)
    assert response.status_code == 200
    assert response.json()["id"] == user_id

@pytest.mark.parametrize("data", test_data)
def test_get_user_by_id_with_csv(data):
    user_id = data["id"]
    expected_name = data["name"]
    response = get_user_by_id(user_id)
    assert response.status_code == 200
    assert response.json()["name"] == expected_name

testcases/test_mock_api.py

# testcases/test_mock_api.py
import pytest
import respx
from httpx import Response
from api.user_api import get_user_by_id

@respx.mock
def test_get_user_by_id_mock():
    user_id = 1
    mock_response = {"id": user_id, "name": "Mock User"}
    respx.get(f"https://jsonplaceholder.typicode.com/users/{user_id}").mock(
        return_value=Response(200, json=mock_response)
    )
    response = get_user_by_id(user_id)
    assert response.status_code == 200
    assert response.json() == mock_response

Running Tests & Viewing Reports

run_tests.py

# run_tests.py
import pytest
import os

if __name__ == "__main__":
    report_dir = os.path.join(os.getcwd(), "reports", "allure")
    if not os.path.exists(report_dir):
        os.makedirs(report_dir)
    pytest.main([
        "-v",
        "--alluredir=./reports/allure",
        "-n", "auto",  # parallel execution
        "testcases/"
    ])
    # generate Allure report
    os.system('allure serve ./reports/allure')

Note: Install the Allure command‑line tool and add it to your system PATH before running the script.

Optional Extensions (Suggested)

Asynchronous test cases: add test_async_api.py under testcases/ CI/CD integration: Jenkins or GitHub Actions templates

Token authentication mechanism: encapsulate login token retrieval

Test coverage statistics using coverage.py Database verification with pymysql or

SQLAlchemy
AutomationAsyncAPI testingpytestAllurehttpx
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.