#5 - Pytest API Automation Framework - Setup

• Possess over 14 years of experience in Quality Engineering, specializing in building test frameworks for complex systems including web applications, REST APIs, and core cloud infrastructure on platforms like Akamai's Linode and Azure.
• Skilled in the development and improvement of automation frameworks for various Web applications, service APIs displaying proficiency in TestNG, PyTest, Selenium, Rest Assured, Jenkins pipeline.
This article explains a pytest-based API automation framework. The framework is divided into three clear parts:
The framework follows a 3-layer architecture:
Core – low-level HTTP and logging
Application – business logic and payloads
Tests – pytest tests and fixtures
Code - https://github.com/sksingh329/gorest-api-test.git
Framework Architecture

Core Layer – The Foundation
The core layer handles everything related to HTTP communication and logging.
gorest-api-test/
├── core/
│ ├── api/
│ │ └── api_client.py → APIClient
│ └── utils/
│ └── http_logger.py
core/api/api_client.py
This is a thin wrapper over the requests library.
Make HTTP calls (GET, POST, PUT, DELETE)
Attach headers and base URL
Return raw responses
utils/http_logger.py → Handles request and response logging.
class APIClient:
def __init__(self, base_url, timeout=30, headers=None):
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self.headers = headers or {}
def get(self, path, headers=None, params=None):
url = f"{self.base_url}/{path.lstrip('/')}"
final_headers = {**self.headers, **(headers or {})}
log_request(
method="GET",
url=url,
params=params,
headers=final_headers,
)
response = requests.get(
url,
headers=final_headers,
params=params,
timeout=self.timeout
)
log_response(response)
return response
Application Layer – Business Logic
gorest-api-test/
├── application/
│ ├── user_client.py
│ ├── payload/
│ │ └── user_payload.py
This layer represents how your application behaves, not how HTTP works.
application/user_client.py
- This is where business logic defined which tests will call for setup and performing test action.
payloads/user_payload.py → Handles test data creation.
class UserClient:
def __init__(self, api_client):
self.api_client = api_client
def list_user(self):
return self.api_client.get(USERS_ENDPOINT)
def create_user(self, payload):
return self.api_client.post(USERS_ENDPOINT, body=payload)
def get_user(self, user_id):
return self.api_client.get(USER_ENDPOINT.format(user_id=user_id))
def update_user(self, user_id, payload):
return self.api_client.put(USER_ENDPOINT.format(user_id=user_id), body=payload)
def delete_user(self, user_id):
return self.api_client.delete(USER_ENDPOINT.format(user_id=user_id))
Tests Layer – Where Assertions Live
├── tests/
│ ├── conftest.py
│ ├── test_users_collection.py
│ ├── user/
│ │ ├── conftest.py
│ │ └── test_user_resource.py
tests/conftest.py → Global pytest configuration and common fixtures.
tests/users/conftest.py → User-specific fixtures.
Test Files
Call methods from UserClient
Assert response status and data
conftest.py
@pytest.fixture(scope="session")
def auth_headers():
token = os.getenv("API_TOKEN")
return {
"Authorization": f"Bearer {token}"
}
@pytest.fixture
def api_client(auth_headers):
return APIClient(
base_url=BASE_URL,
timeout=20,
headers=auth_headers
)
@pytest.fixture
def user_client(api_client):
return UserClient(api_client=api_client)
test_users_collection.py
def test_create_user(user_client):
payload = create_user_payload(status="inactive")
response = user_client.create_user(payload=payload)
response_body = response.json()
assert response.status_code == 201
assert response_body["name"] == payload["name"]
assert response_body["email"] == payload["email"]
assert response_body["gender"] == payload["gender"]
assert response_body["status"] == payload["status"]
user_id = response_body["id"]
logger.info(f"User ID: {user_id}")
if user_id:
user_client.delete_user(user_id=user_id)
users/conftest.py
@pytest.fixture
def user_fixture(user_client):
payload = user_create_payload()
response = user_client.create_user(payload=payload)
assert response.status_code == 201
response_body = response.json()
user_id = response_body["id"]
logger.info(f"Created user with ID: {user_id}")
yield user_id
user_client.delete_user(user_id=user_id)
logger.info(f"Deleted user with ID: {user_id}")
users/test_user_resource.py
def test_get_user(user_client, user_fixture):
user_id = user_fixture
response = user_client.get_user(user_id=user_id)
assert response.status_code == 200
def test_update_user(user_client, user_fixture):
# Update user status to active
payload = user_update_payload(status="active")
user_id = user_fixture
response = user_client.update_user(user_id=user_id, payload=payload)
assert response.status_code == 200
assert response.json()["status"] == "active"
Execution Flow
pytest starts
Fixtures are loaded from conftest.py
UserClient uses APIClient
APIClient makes HTTP calls
Requests & responses are logged
Tests assert results

Running tests
# Setup
git clone https://github.com/sksingh329/gorest-api-test.git
cd gorest-api-test
uv sync
export API_TOKEN="<<go_rest_api_token>>"
# Running tests
uv run pytest