January 30, 2026

#5 - Pytest API Automation Framework - Setup

api-testingpythonpytestframework

This article walks through a pytest-based API automation framework, structured around a 3-layer architecture:

  • Core — low-level HTTP and logging
  • Application — business logic and payloads
  • Tests — pytest tests and fixtures

Code: gorest-api-test on GitHub

Framework architecture

Framework diagram

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 that makes HTTP calls (GET, POST, PUT, DELETE), attaches headers and base URL, and returns 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 the business logic lives that tests call for setup and for performing the 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 holds global pytest configuration and common fixtures. tests/users/conftest.py holds user-specific fixtures.

Test files call methods from UserClient and assert on 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 and responses are logged
  • Tests assert results

Execution flow diagram

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

Originally published on Hashnode.