January 30, 2026
#5 - Pytest API Automation Framework - Setup
Part 5 of 5 · API Testing | Pytest & requests
- 1. #1 - GoRest CRUD APIs with curl
- 2. #2 - Setting Up a Python API Testing Project with uv
- 3. #3 - API Testing with requests
- 4. #4 - Pytest Concept
- 5. #5 - Pytest API Automation Framework - Setup
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

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.pycore/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 responseApplication layer — business logic
gorest-api-test/
├── application/
│ ├── user_client.py
│ ├── payload/
│ │ └── user_payload.pyThis 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.pytests/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 UserClientusesAPIClientAPIClientmakes HTTP calls- Requests and 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 pytestOriginally published on Hashnode.