Skip to content

Framework Extensibility & Anti-Patterns

Framework Extensibility

A good framework grows without rewriting its core.

Custom pytest Plugins

Encapsulate framework-level behaviour in a local plugin:

# framework/plugin.py
import pytest
import logging

log = logging.getLogger(__name__)


def pytest_runtest_setup(item: pytest.Item) -> None:
    log.info("Starting test: %s", item.nodeid)


def pytest_runtest_teardown(item: pytest.Item, nextitem) -> None:
    log.info("Finished test: %s", item.nodeid)


def pytest_runtest_logreport(report: pytest.TestReport) -> None:
    if report.when == "call":
        status = "PASSED" if report.passed else "FAILED"
        log.info("Test %s: %s in %.2fs", status, report.nodeid, report.duration)

Register in conftest.py:

pytest_plugins = ["framework.plugin"]

Custom Reporters

# framework/reporters/slack_reporter.py
import pytest
import httpx
import logging

log = logging.getLogger(__name__)


class SlackReporter:
    def __init__(self, webhook_url: str) -> None:
        self._webhook = webhook_url
        self._failures: list[str] = []

    def record_failure(self, nodeid: str) -> None:
        self._failures.append(nodeid)

    def send_summary(self, total: int, duration: float) -> None:
        if not self._failures:
            return
        message = (
            f":x: *Test failures*: {len(self._failures)}/{total}\n"
            f"Duration: {duration:.1f}s\n"
            + "\n".join(f"• `{f}`" for f in self._failures[:10])
        )
        httpx.post(self._webhook, json={"text": message})
        log.info("Slack report sent: %d failures", len(self._failures))

Replaceable Components

Design core components as interfaces (protocols in Python):

from typing import Protocol


class HttpDriverProtocol(Protocol):
    def get(self, path: str, **kwargs) -> object: ...
    def post(self, path: str, **kwargs) -> object: ...
    def close(self) -> None: ...

Tests depend on the protocol, not the implementation. Swap real HTTP driver for a mock driver in unit tests of framework components.

Modularity Rules

  • Each module has one responsibility
  • No circular imports between framework layers
  • Core layer has zero test-domain knowledge
  • New protocol support (gRPC, MQTT) adds a new driver, touches nothing else

Anti-Patterns

God Page Object

# Bad — 200+ methods in one class
class ApplicationPage:
    def login(self): ...
    def logout(self): ...
    def create_product(self): ...
    def update_product(self): ...
    def delete_product(self): ...
    def create_order(self): ...
    def get_user_profile(self): ...
    # ... 190 more methods

Fix: one page object per page or significant component.

Hardcoded Test Data

# Bad — hardcoded values cause collisions and coupling
def test_create_user():
    response = api.post("/users", json={"email": "john@test.com"})
    assert response.status_code == 201

# Good — unique every run
def test_create_user():
    email = f"john-{uuid.uuid4().hex[:8]}@test.com"
    response = api.post("/users", json={"email": email})
    assert response.status_code == 201

Tight Coupling to UI Implementation

# Bad — breaks every time class is renamed
page.locator(".btn-primary.col-md-6.submit-action")

# Good — stable semantic selector
page.get_by_test_id("checkout-submit")

Flaky Tests Ignored

Ignoring flaky tests is a policy decision to accept broken CI. Every flaky test must be either fixed or quarantined with a tracking ticket. Silence = acceptance of degraded feedback quality.

No Abstraction in Tests

# Bad — HTTP construction mixed into test
def test_user_can_update_email(api_token):
    import httpx
    client = httpx.Client(base_url="https://api.example.com")
    headers = {"Authorization": f"Bearer {api_token}"}
    user = client.post("/users", headers=headers, json={"email": "u@e.com"}).json()
    response = client.patch(
        f"/users/{user['id']}",
        headers=headers,
        json={"email": "new@e.com"},
    )
    assert response.status_code == 200

# Good — tests describe intent, not protocol
def test_user_can_update_email(api_client, user_factory):
    user = user_factory.create()
    api_client.users.update(user["id"], email="new@example.com")
    updated = api_client.users.get(user["id"])
    assert updated["email"] == "new@example.com"

Test Implementation Coupling

# Bad — test knows internal implementation details
def test_discount_applied(order_service, mocker):
    mocker.patch.object(order_service, "_calculate_discount_internally")
    # Testing that a private method was called = implementation test, not behaviour test

# Good — test verifies observable outcome
def test_discount_applied(order_service):
    order = order_service.create(items=["SKU-001"], promo_code="SAVE20")
    assert order["total"] == order["subtotal"] * 0.8