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.
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 documentationInstallation
pip install httpx pytest pytest-html allure-pytest respx pandas pytest-xdistSave 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.0Configuration 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 responseLogging 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_nametestcases/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_responseRunning 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
SQLAlchemyHow this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
