Back to list
dawiddutoit

pytest-type-safety

by dawiddutoit

Collection of Claude Code skills, agents, and plugins

0🍴 0📅 Jan 20, 2026

SKILL.md


name: pytest-type-safety description: | Enforce type safety in all tests: type annotations for test functions and fixtures, mock autospec patterns, mypy configuration for tests, type-safe assertions, generic type handling in mocks. Includes protocol-based typing and cast patterns for complex scenarios.

Pytest Type Safety

Purpose

Type safety in tests catches errors early and improves code quality. This skill provides patterns for enforcing strict types throughout your test suite.

Quick Start

Add type annotations to every test function and fixture:

from __future__ import annotations

from typing import Any, AsyncGenerator, Optional
from unittest.mock import AsyncMock
import pytest

from app.extraction.domain.entities import Order
from app.extraction.application.use_cases import ExtractOrdersUseCase

# ✅ GOOD: Type annotations on fixture
@pytest.fixture
def create_test_order() -> Any:  # Return type of factory function
    """Factory fixture with type hints."""
    def _create(order_id: str = "123") -> Order:
        return Order(...)
    return _create

# ✅ GOOD: Type annotations on test function
@pytest.mark.asyncio
async def test_extract_orders(
    create_test_order: Any,
    mock_gateway: AsyncMock,
) -> None:
    """Test function with full type safety."""
    use_case = ExtractOrdersUseCase(gateway=mock_gateway)
    result = await use_case.execute()
    assert result is not None

Instructions

Step 1: Add Type Annotations to All Test Functions

from __future__ import annotations

import pytest
from app.extraction.domain.value_objects import ProductTitle


# ❌ BAD: No type annotations
def test_product_title():
    title = ProductTitle("Laptop")
    assert title.value == "Laptop"


# ✅ GOOD: Complete type annotations
def test_product_title() -> None:
    """Test product title creation."""
    title: ProductTitle = ProductTitle("Laptop")
    assert title.value == "Laptop"


# ✅ GOOD: Parametrized with type annotations
@pytest.mark.parametrize(
    "input_title,expected",
    [("Laptop", "Laptop"), ("Mouse", "Mouse")],
    ids=["laptop", "mouse"]
)
def test_valid_titles(input_title: str, expected: str) -> None:
    """Test title parametrization."""
    title = ProductTitle(input_title)
    assert title.value == expected

Step 2: Type Annotate All Fixtures

from __future__ import annotations

from typing import Any, AsyncGenerator, Callable
from unittest.mock import AsyncMock
import pytest

from app.extraction.domain.entities import Order, LineItem
from app.extraction.domain.value_objects import OrderId, ProductId, ProductTitle, Money


# ✅ GOOD: Factory fixture with return type
@pytest.fixture
def create_test_line_item() -> Callable[..., LineItem]:
    """Factory for creating line items."""
    def _create(
        product_id: str = "prod_1",
        title: str = "Test Product",
        quantity: int = 1,
        price: float = 99.99,
    ) -> LineItem:
        return LineItem(
            product_id=ProductId(product_id),
            product_title=ProductTitle(title),
            quantity=quantity,
            price=Money.from_float(price),
        )

    return _create


# ✅ GOOD: Async fixture with proper return type
@pytest.fixture
async def async_http_client() -> AsyncGenerator[AsyncClient, None]:
    """Async HTTP client fixture."""
    from httpx import AsyncClient
    from app.main import app

    async with AsyncClient(app=app, base_url="http://test") as client:
        yield client


# ✅ GOOD: Mock fixture with return type
@pytest.fixture
def mock_gateway() -> AsyncMock:
    """Mock gateway with proper type."""
    mock = AsyncMock()
    mock.fetch_orders.return_value = []
    return mock


# ✅ GOOD: Parametrized fixture with type
@pytest.fixture(params=["http://api1.com", "http://api2.com"])
def api_endpoint(request: pytest.FixtureRequest) -> str:
    """Parametrized endpoint fixture."""
    return request.param

Step 3: Use Create_Autospec for Type-Safe Mocks

from __future__ import annotations

from typing import Any
from unittest.mock import AsyncMock, create_autospec
import pytest

from app.extraction.application.ports import ShopifyPort


# ❌ BAD: Mock without autospec (allows typos)
def test_bad_mock(mocker: Any) -> None:
    """Mock without autospec allows invalid calls."""
    mock = mocker.Mock()
    mock.typo_method()  # No error! Dangerous!


# ✅ GOOD: Mock with autospec enforces interface
def test_with_autospec() -> None:
    """Mock with autospec catches typos."""
    mock = create_autospec(ShopifyPort, instance=True)
    # mock.typo_method() raises AttributeError (good!)
    mock.fetch_orders.return_value = []


# ✅ GOOD: Async autospec with proper type
@pytest.fixture
def typed_mock_gateway() -> AsyncMock:
    """Type-safe mock gateway."""
    mock = create_autospec(ShopifyPort, instance=True)
    mock.fetch_orders = AsyncMock(return_value=[])
    return mock

Step 4: Configure Mypy for Test Files

# pyproject.toml

[tool.mypy]
python_version = "3.11"
strict = true
disallow_untyped_defs = true
disallow_any_unimported = true
disallow_untyped_calls = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unused_configs = true
show_error_codes = true

# Type check test files
[[tool.mypy.overrides]]
module = "tests.*"
# Still enforce type checking
disallow_untyped_defs = true
# But allow Any from pytest fixtures
disallow_any_unimported = false

# Allow Any for mocker fixture
[[tool.mypy.overrides]]
module = "pytest_mock"
ignore_missing_imports = true

Step 5: Type-Safe Assertions

from __future__ import annotations

from typing import Any
import pytest

from app.extraction.domain.entities import Order


def test_order_type_safe(create_test_order: Any) -> None:
    """Type-safe assertions with explicit types."""
    order: Order = create_test_order()

    # ✅ GOOD: Explicit type check
    assert isinstance(order, Order)
    assert len(order.line_items) > 0

    # ✅ GOOD: Type is known from variable
    assert order.customer_name is not None
    assert isinstance(order.customer_name, str)

    # ✅ GOOD: Compare with typed variables
    expected_id: str = "order_123"
    assert order.order_id.value == expected_id


def test_list_items_typed() -> None:
    """Type-safe list assertions."""
    items: list[str] = ["laptop", "mouse", "keyboard"]

    # ✅ GOOD: Check type and content
    assert isinstance(items, list)
    assert len(items) == 3
    assert all(isinstance(item, str) for item in items)

Step 6: Use Typing.Cast for Complex Mocks

from __future__ import annotations

from typing import cast, Any
from unittest.mock import AsyncMock
import pytest

from app.extraction.application.ports import ShopifyPort


def test_with_cast() -> None:
    """Use cast when mock type doesn't match interface."""
    # Create mock
    mock = AsyncMock()

    # Cast to interface type for type checker
    gateway: ShopifyPort = cast(ShopifyPort, mock)

    # Now type checker knows gateway has ShopifyPort interface
    # But we still have mock's flexibility at runtime
    gateway.fetch_orders.return_value = []

Step 7: Use Protocols for Type-Safe Mocking

from __future__ import annotations

from typing import Protocol, Any
from unittest.mock import AsyncMock, create_autospec
import pytest


class OrderRepository(Protocol):
    """Protocol defining repository interface."""

    async def create(self, data: dict[str, Any]) -> Order:
        """Create order."""
        ...

    async def get_by_id(self, order_id: str) -> Order | None:
        """Get order by ID."""
        ...


# ✅ GOOD: Mock respects protocol
@pytest.fixture
def mock_repository() -> AsyncMock:
    """Type-safe mock using protocol."""
    mock = create_autospec(OrderRepository, instance=True)
    mock.create = AsyncMock(return_value=Order(...))
    mock.get_by_id = AsyncMock(return_value=None)
    return mock


def test_with_typed_mock(mock_repository: AsyncMock) -> None:
    """Test using typed mock."""
    use_case = CreateOrderUseCase(repository=mock_repository)
    # Type checker knows mock_repository conforms to OrderRepository

Step 8: Type Safe Parametrization

from __future__ import annotations

from typing import NamedTuple
import pytest


class OrderTestCase(NamedTuple):
    """Type-safe parametrization data."""

    order_id: str
    customer_name: str
    total: float


# ✅ GOOD: Parametrize with typed data
ORDER_TEST_CASES: list[OrderTestCase] = [
    OrderTestCase(order_id="1", customer_name="Alice", total=100.0),
    OrderTestCase(order_id="2", customer_name="Bob", total=200.0),
]


@pytest.mark.parametrize(
    "test_case",
    ORDER_TEST_CASES,
    ids=[tc.customer_name for tc in ORDER_TEST_CASES]
)
def test_orders_typed(test_case: OrderTestCase) -> None:
    """Parametrized test with type safety."""
    order = create_test_order(
        order_id=test_case.order_id,
        customer_name=test_case.customer_name,
        total=test_case.total,
    )

    assert order.order_id.value == test_case.order_id

Step 9: Type Check Test Files in CI/CD

# GitHub Actions
name: Type Check

on: [push, pull_request]

jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

      - name: Install dependencies
        run: uv sync

      - name: Type check with mypy
        run: uv run mypy tests/

      - name: Type check with pyright
        run: uv run pyright tests/

Step 10: Use Typing.Assert_Type for Verification

from __future__ import annotations

from typing import assert_type
import pytest

from app.extraction.domain.entities import Order


def test_return_type_is_correct(create_test_order: Any) -> None:
    """Verify return types are correct."""
    order = create_test_order()

    # Compile-time check that order is Order
    assert_type(order, Order)

    # Runtime check
    assert isinstance(order, Order)

Examples

Example 1: Complete Type-Safe Test File

from __future__ import annotations

from typing import Any, AsyncGenerator, Callable
from unittest.mock import AsyncMock, create_autospec
import pytest
from httpx import AsyncClient, ASGITransport

from app.extraction.domain.entities import Order, LineItem
from app.extraction.domain.value_objects import OrderId, ProductId, ProductTitle, Money
from app.extraction.application.use_cases import ExtractOrdersUseCase
from app.extraction.application.ports import ShopifyPort, PublisherPort
from app.main import app


# Fixtures
@pytest.fixture
def create_test_line_item() -> Callable[..., LineItem]:
    """Factory for line items."""
    def _create(
        product_id: str = "prod_1",
        title: str = "Product",
        quantity: int = 1,
        price: float = 99.99,
    ) -> LineItem:
        return LineItem(
            product_id=ProductId(product_id),
            product_title=ProductTitle(title),
            quantity=quantity,
            price=Money.from_float(price),
        )

    return _create


@pytest.fixture
def create_test_order(create_test_line_item: Callable[..., LineItem]) -> Callable[..., Order]:
    """Factory for orders."""
    def _create(
        order_id: str = "order_123",
        customer_name: str = "Test User",
    ) -> Order:
        items: list[LineItem] = [create_test_line_item()]
        total: float = sum(
            float(item.price.amount) * item.quantity for item in items
        )

        return Order(
            order_id=OrderId(order_id),
            created_at=datetime.now(),
            customer_name=customer_name,
            line_items=items,
            total_price=Money.from_float(total),
        )

    return _create


@pytest.fixture
def mock_shopify_gateway() -> AsyncMock:
    """Type-safe mock gateway."""
    mock = create_autospec(ShopifyPort, instance=True)
    mock.fetch_orders = AsyncMock(return_value=[])
    return mock


@pytest.fixture
def mock_event_publisher() -> AsyncMock:
    """Type-safe mock publisher."""
    mock = create_autospec(PublisherPort, instance=True)
    mock.publish_order = AsyncMock(return_value=None)
    mock.close = AsyncMock(return_value=None)
    return mock


@pytest.fixture
async def http_client() -> AsyncGenerator[AsyncClient, None]:
    """Typed async HTTP client."""
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        yield client


# Tests
class TestExtractOrdersUseCase:
    """Type-safe test class."""

    @pytest.mark.asyncio
    async def test_execute_success(
        self,
        mock_shopify_gateway: AsyncMock,
        mock_event_publisher: AsyncMock,
    ) -> None:
        """Test with full type safety."""
        use_case: ExtractOrdersUseCase = ExtractOrdersUseCase(
            gateway=mock_shopify_gateway,
            publisher=mock_event_publisher,
        )

        result = await use_case.execute()

        assert result is not None
        assert isinstance(result.orders_count, int)
        assert result.orders_count >= 0

Example 2: Type-Safe Test Data

from dataclasses import dataclass
from typing import Any


@dataclass
class MockOrderData:
    """Typed mock order data."""

    order_id: str
    customer_name: str
    total: float
    line_count: int


MOCK_ORDERS: list[MockOrderData] = [
    MockOrderData(order_id="1", customer_name="Alice", total=100.0, line_count=1),
    MockOrderData(order_id="2", customer_name="Bob", total=200.0, line_count=2),
]


@pytest.mark.parametrize("order_data", MOCK_ORDERS)
def test_with_typed_data(order_data: MockOrderData) -> None:
    """Type-safe parametrized test."""
    order = create_test_order(
        order_id=order_data.order_id,
        customer_name=order_data.customer_name,
    )

    assert order.order_id.value == order_data.order_id

Requirements

  • Python 3.11+
  • pytest >= 7.0
  • mypy >= 1.7.0
  • pyright >= 1.1.0 (optional)
  • Unit test code with type annotations

See Also

Score

Total Score

50/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon