<< All versions
Skill v1.0.0
currentAutomated scan100/100izangi2714/claude-code-python-stack/pytest-oop-patterns
──Details
PublishedSeptember 28, 2026 at 06:31 PM
Content Hashsha256:3b68d38e48875ff9...
Git SHA
──Files
Files (1 file, 8.8 KB)
SKILL.md8.8 KBactive
SKILL.md · 314 lines · 8.8 KB
version: "1.0.0" name: pytest-oop-patterns description: OOP-based pytest patterns for API test automation — base classes, service layers, fixtures hierarchy, parametrization, markers, parallel execution, and test isolation. origin: custom
OOP Pytest Patterns for API Testing
Strict OOP patterns for building maintainable, scalable pytest test suites.
When to Activate
- Designing test framework architecture
- Writing new API test suites
- Reviewing test code for OOP compliance
- Setting up pytest infrastructure (conftest, fixtures, markers)
RULE: No Bare Functions
Every test MUST be inside a class. This is non-negotiable.
python
# WRONG -- procedural, no encapsulationdef test_create_user():response = httpx.post("/users", json={"email": "a@b.com"})assert response.status_code == 201# CORRECT -- OOP, inherits BaseTest, uses service class@allure.feature("User Management")class TestCreateUser(BaseTest):def test_create_user_returns_201(self):data = self.gen.create_user_request()response = self.users_api.create_user(data)assert response.status_code == 201
Base Test Class
python
import pytestimport allurefrom framework.clients.http_client import HTTPClientfrom framework.config.settings import Settingsclass BaseTest:"""Base class for ALL test classes."""client: HTTPClientsettings: Settings@pytest.fixture(autouse=True)def _setup_base(self, http_client: HTTPClient, settings: Settings):"""Inject shared dependencies."""self.client = http_clientself.settings = settingsclass BaseAuthenticatedTest(BaseTest):"""Base for tests requiring authentication."""@pytest.fixture(autouse=True)def _setup_auth(self, authenticated_client: HTTPClient):self.client = authenticated_client
API Service Layer
python
from framework.base.base_api_client import BaseAPIClientclass BaseAPIClient:"""Base for all API service classes."""def __init__(self, client: HTTPClient):self.client = clientclass UsersAPI(BaseAPIClient):ENDPOINT = "/api/v1/users"@allure.step("POST /users — Create user: {data.email}")def create_user(self, data: CreateUserRequest) -> httpx.Response:return self.client.post(self.ENDPOINT, json=data.model_dump())@allure.step("GET /users/{user_id}")def get_user(self, user_id: int) -> httpx.Response:return self.client.get(f"{self.ENDPOINT}/{user_id}")@allure.step("GET /users — List (page={page})")def list_users(self, page: int = 1, per_page: int = 20) -> httpx.Response:return self.client.get(self.ENDPOINT, params={"page": page, "per_page": per_page})@allure.step("PATCH /users/{user_id}")def update_user(self, user_id: int, data: dict) -> httpx.Response:return self.client.patch(f"{self.ENDPOINT}/{user_id}", json=data)@allure.step("DELETE /users/{user_id}")def delete_user(self, user_id: int) -> httpx.Response:return self.client.delete(f"{self.ENDPOINT}/{user_id}")
Fixtures Hierarchy
Root conftest.py (session-scoped)
python
# conftest.pyimport pytestfrom framework.clients.http_client import HTTPClientfrom framework.config.settings import Settings@pytest.fixture(scope="session")def settings() -> Settings:return Settings()@pytest.fixture(scope="session")def http_client(settings: Settings) -> HTTPClient:client = HTTPClient(settings)yield clientclient.close()@pytest.fixture(scope="session")def auth_token(http_client, settings) -> str:from api.auth_api import AuthAPIauth = AuthAPI(http_client)resp = auth.login(settings.TEST_USER_EMAIL, settings.TEST_USER_PASSWORD)return resp.json()["access_token"]@pytest.fixturedef authenticated_client(settings, auth_token) -> HTTPClient:client = HTTPClient(settings, token=auth_token)yield clientclient.close()
Module conftest.py (test-specific)
python
# tests/test_users/conftest.pyimport pytestfrom api.users_api import UsersAPIfrom framework.helpers.data_generator import DataGenerator@pytest.fixturedef users_api(http_client) -> UsersAPI:return UsersAPI(http_client)@pytest.fixturedef created_user(users_api: UsersAPI) -> dict:"""Create a user and return response body. Cleanup after test."""data = DataGenerator.create_user_request()response = users_api.create_user(data)user = response.json()yield userusers_api.delete_user(user["id"]) # Cleanup
Parametrization Patterns
Data-driven tests
python
@allure.feature("Input Validation")class TestUserValidation(BaseTest):@pytest.mark.parametrize("field,value,error_field", [("email", "not-email", "email"),("email", "", "email"),("password", "123", "password"),("name", "", "name"),("name", "x" * 256, "name"),], ids=["invalid-email-format","empty-email","short-password","empty-name","name-too-long",])@allure.story("Field validation")def test_create_user_field_validation(self, field: str, value: str, error_field: str):data = self.gen.create_user_request(**{field: value})response = self.users_api.create_user(data)assert response.status_code == 422errors = response.json()assert any(error_field in str(e) for e in errors.get("detail", []))
Status code matrix
python
@allure.feature("Authorization")class TestUserAuthorization(BaseTest):@pytest.mark.parametrize("method,endpoint,expected", [("GET", "/api/v1/users", 401),("POST", "/api/v1/users", 401),("GET", "/api/v1/users/1", 401),("DELETE", "/api/v1/users/1", 401),], ids=["list", "create", "get", "delete"])@allure.story("Unauthorized access returns 401")def test_unauthenticated_returns_401(self, method: str, endpoint: str, expected: int):response = getattr(self.client, method.lower())(endpoint)assert response.status_code == expected
Markers
ini
# pyproject.toml[tool.pytest.ini_options]markers = ["smoke: Quick sanity checks (< 30 seconds total)","regression: Full regression suite","critical: Business-critical flows","negative: Negative/error scenarios","slow: Tests > 10 seconds","flaky: Known flaky tests (quarantined)",]
python
@pytest.mark.smoke@pytest.mark.criticalclass TestAuthSmoke(BaseAuthenticatedTest):def test_login_returns_token(self): ...def test_token_refresh_works(self): ...
bash
# Run smoke suitepytest -m smoke --alluredir=allure-results# Run everything except flakypytest -m "not flaky"# Run critical regressionpytest -m "critical and regression"
Parallel Execution
bash
# Installpip install pytest-xdist# Run parallel (auto-detect CPU count)pytest -n auto --alluredir=allure-results# Fixed worker countpytest -n 4
Rules for parallel-safe tests:
- No shared mutable state (class variables, global dicts)
- Each test creates its own entities
- Fixtures with
scope="session"must be thread-safe - Use unique identifiers (UUID) in test data, not sequential IDs
Custom Assertions
python
import allurefrom framework.models.response_models import ErrorResponseclass Assertions:"""Custom assertion helpers with Allure integration."""@staticmethod@allure.step("Assert status code is {expected}")def assert_status(response, expected: int):assert response.status_code == expected, (f"Expected {expected}, got {response.status_code}. "f"Body: {response.text[:200]}")@staticmethod@allure.step("Assert response matches schema {model.__name__}")def assert_schema(response, model: type):"""Validate response body against Pydantic model."""data = response.json()parsed = model.model_validate(data)return parsed@staticmethod@allure.step("Assert error response contains: {expected_detail}")def assert_error(response, expected_status: int, expected_detail: str | None = None):assert response.status_code == expected_statusif expected_detail:error = ErrorResponse.model_validate(response.json())assert expected_detail in error.detail
Quick Reference
| Pattern | Description | |
|---|---|---|
BaseTest | All test classes inherit from this | |
BaseAPIClient | All API services inherit from this | |
@allure.step | Every API method gets this decorator | |
DataGenerator | Factory for test data (faker-based) | |
Assertions | Custom assertion helpers | |
conftest.py hierarchy | Root (session) -> module -> suite | |
@pytest.mark.parametrize | Data-driven testing | |
pytest -n auto | Parallel execution | |
| Arrange-Act-Assert | Mandatory test structure |