Agent Skill
2/7/2026

pytest-patterns

Modern pytest test structure with fixtures, parametrization, and the AAA pattern. Use this skill when writing tests, creating test suites, or implementing test strategies. MUST BE USED for: pytest tests, test fixtures, parametrization, mocking, async tests, test organization. Keywords: pytest, test, fixture, parametrize, mock, assert, conftest.

A
agentient
0GitHub Stars
1Views
npx skills add Agentient/vibekit

SKILL.md

Namepytest-patterns
DescriptionModern pytest test structure with fixtures, parametrization, and the AAA pattern. Use this skill when writing tests, creating test suites, or implementing test strategies. MUST BE USED for: pytest tests, test fixtures, parametrization, mocking, async tests, test organization. Keywords: pytest, test, fixture, parametrize, mock, assert, conftest.

name: pytest-patterns description: | Modern pytest test structure with fixtures, parametrization, and the AAA pattern. Use this skill when writing tests, creating test suites, or implementing test strategies. MUST BE USED for: pytest tests, test fixtures, parametrization, mocking, async tests, test organization. Keywords: pytest, test, fixture, parametrize, mock, assert, conftest.

Pytest Modern Test Structure

Core Principles

All Python code in Vibekit MUST achieve 80%+ test coverage. This skill provides authoritative patterns for writing clean, maintainable, and effective tests using pytest.

The AAA Pattern (Arrange-Act-Assert)

Every test should follow the AAA structure:

import pytest

# ✅ REQUIRED: Use AAA pattern for all tests
def test_user_creation():
    # ARRANGE: Set up test data and dependencies
    user_data = {
        "email": "test@example.com",
        "username": "testuser",
        "age": 25
    }

    # ACT: Execute the code under test
    user = User.create(user_data)

    # ASSERT: Verify the outcome
    assert user.email == "test@example.com"
    assert user.username == "testuser"
    assert user.age == 25
    assert user.id is not None

# ❌ FORBIDDEN: Tests without clear structure
def bad_test():
    user = User.create({"email": "test@example.com", "username": "testuser"})
    assert user.email == "test@example.com"
    user.update({"age": 30})
    assert user.age == 30
    # Mixing arrange/act/assert makes tests hard to understand

Fixtures for Test Dependencies

Fixtures provide reusable test setup and teardown:

import pytest
from typing import Generator

# ✅ REQUIRED: Use fixtures for shared setup
@pytest.fixture
def sample_user() -> User:
    """Create a sample user for testing."""
    return User(
        id=1,
        email="test@example.com",
        username="testuser"
    )

@pytest.fixture
def database_connection() -> Generator:
    """Provide database connection with cleanup."""
    # Setup
    conn = connect_to_test_db()

    yield conn  # Provide to test

    # Teardown (always runs, even if test fails)
    conn.close()

# Using fixtures in tests
def test_user_retrieval(database_connection, sample_user):
    # ARRANGE: Dependencies injected via fixtures
    db = database_connection

    # ACT
    retrieved = db.get_user(sample_user.id)

    # ASSERT
    assert retrieved.id == sample_user.id
    assert retrieved.email == sample_user.email

Fixture Scopes

Control fixture lifecycle with scopes:

import pytest

# ✅ REQUIRED: Use narrowest scope possible
@pytest.fixture(scope="function")  # Default: runs for each test
def user():
    return User(id=1, email="test@example.com")

@pytest.fixture(scope="module")  # Runs once per test module
def expensive_setup():
    # Expensive operation
    return initialize_test_environment()

@pytest.fixture(scope="session")  # Runs once per test session
def database_connection():
    """Shared database for all tests."""
    conn = connect_to_test_db()
    yield conn
    conn.close()

# ❌ FORBIDDEN: Using broad scope for mutable fixtures
@pytest.fixture(scope="session")  # BAD! Can cause test interdependency
def user_list():
    return []  # Mutable state shared across tests - dangerous!

# ✅ GOOD: Use function scope for mutable data
@pytest.fixture(scope="function")
def user_list():
    return []  # Fresh list for each test

Parametrized Tests

Test multiple inputs without code duplication:

import pytest

# ✅ REQUIRED: Use parametrize for testing multiple cases
@pytest.mark.parametrize("email,expected_valid", [
    ("user@example.com", True),
    ("user@subdomain.example.com", True),
    ("invalid-email", False),
    ("@example.com", False),
    ("user@", False),
])
def test_email_validation(email: str, expected_valid: bool):
    # ARRANGE
    validator = EmailValidator()

    # ACT
    result = validator.is_valid(email)

    # ASSERT
    assert result == expected_valid

# Multiple parameters
@pytest.mark.parametrize("age,income,expected_eligible", [
    (18, 30000, True),
    (17, 30000, False),
    (18, 20000, False),
    (25, 50000, True),
])
def test_loan_eligibility(age: int, income: int, expected_eligible: bool):
    assert check_loan_eligibility(age, income) == expected_eligible

# ❌ FORBIDDEN: Conditional logic in parametrized tests
@pytest.mark.parametrize("value,expected", [(1, 2), (2, 4), (3, 6)])
def bad_test(value: int, expected: int):
    result = double(value)
    if value == 2:  # BAD! Test behavior changes based on parameter
        assert result > 0
    else:
        assert result == expected

# ✅ GOOD: Split into separate tests if logic differs
@pytest.mark.parametrize("value,expected", [(1, 2), (3, 6)])
def test_double_positive(value: int, expected: int):
    assert double(value) == expected

def test_double_special_case():
    assert double(2) > 0

Mocking with pytest

Isolate code under test using mocks:

import pytest
from unittest.mock import Mock, patch, MagicMock

# ✅ REQUIRED: Use patch for external dependencies
def test_api_client_with_mock():
    # ARRANGE
    mock_response = Mock()
    mock_response.json.return_value = {"user_id": 123, "name": "Test User"}
    mock_response.status_code = 200

    with patch('requests.get', return_value=mock_response) as mock_get:
        client = APIClient()

        # ACT
        user = client.get_user(123)

        # ASSERT
        assert user["user_id"] == 123
        assert user["name"] == "Test User"
        mock_get.assert_called_once_with("https://api.example.com/users/123")

# Using patch as decorator
@patch('module.expensive_function')
def test_with_decorated_mock(mock_expensive):
    # ARRANGE
    mock_expensive.return_value = 42

    # ACT
    result = function_that_calls_expensive()

    # ASSERT
    assert result == 42
    mock_expensive.assert_called_once()

# Mock side effects
def test_retry_logic():
    # ARRANGE
    mock_api = Mock()
    # First call raises, second succeeds
    mock_api.fetch.side_effect = [ConnectionError("Network issue"), {"data": "success"}]

    # ACT
    result = retry_fetch(mock_api)

    # ASSERT
    assert result == {"data": "success"}
    assert mock_api.fetch.call_count == 2

Testing Exceptions

Verify error handling:

import pytest

# ✅ REQUIRED: Use pytest.raises for exception testing
def test_invalid_age_raises_error():
    # ARRANGE
    invalid_data = {"age": -5}

    # ACT & ASSERT
    with pytest.raises(ValueError, match="Age must be positive"):
        User.create(invalid_data)

# Verifying exception attributes
def test_custom_exception_details():
    with pytest.raises(ValidationError) as exc_info:
        validate_input({"bad": "data"})

    # Assert on exception details
    assert exc_info.value.code == "INVALID_INPUT"
    assert "bad" in str(exc_info.value)

# ❌ FORBIDDEN: Try/except for expected exceptions
def bad_exception_test():
    try:
        User.create({"age": -5})
        assert False, "Should have raised ValueError"
    except ValueError:
        pass  # Don't do this - use pytest.raises

Async Tests

Test asynchronous code with pytest-asyncio:

import pytest
import asyncio

# ✅ REQUIRED: Use @pytest.mark.asyncio for async tests
@pytest.mark.asyncio
async def test_async_api_call():
    # ARRANGE
    client = AsyncAPIClient()

    # ACT
    result = await client.fetch_data("https://api.example.com")

    # ASSERT
    assert result["status"] == "success"

# Async fixtures
@pytest.fixture
async def async_database():
    """Async fixture with cleanup."""
    conn = await connect_async_db()
    yield conn
    await conn.close()

@pytest.mark.asyncio
async def test_with_async_fixture(async_database):
    result = await async_database.query("SELECT * FROM users")
    assert len(result) > 0

Test Organization with conftest.py

Share fixtures across multiple test files:

# conftest.py in tests/ directory
import pytest

@pytest.fixture(scope="session")
def database_url():
    """Provide test database URL for all tests."""
    return "postgresql://test:test@localhost/test_db"

@pytest.fixture
def sample_user():
    """Sample user available to all test modules."""
    return User(id=1, email="test@example.com", username="testuser")

# Tests in any test_*.py file can now use these fixtures
def test_user_creation(sample_user):
    assert sample_user.id == 1

Fixture Factories

Create customizable fixtures:

import pytest

# ✅ REQUIRED: Use fixture factories for flexible test data
@pytest.fixture
def user_factory():
    """Factory for creating users with custom attributes."""
    def _create_user(**kwargs):
        defaults = {
            "id": 1,
            "email": "default@example.com",
            "username": "default_user",
            "is_active": True,
        }
        defaults.update(kwargs)
        return User(**defaults)

    return _create_user

# Usage
def test_active_user(user_factory):
    user = user_factory(email="active@example.com", is_active=True)
    assert user.is_active is True

def test_inactive_user(user_factory):
    user = user_factory(email="inactive@example.com", is_active=False)
    assert user.is_active is False

Markers for Test Organization

Categorize and selectively run tests:

import pytest

# ✅ REQUIRED: Use markers to categorize tests
@pytest.mark.unit
def test_user_validation():
    """Fast unit test."""
    assert validate_email("test@example.com") is True

@pytest.mark.integration
def test_database_integration(database_connection):
    """Slower integration test."""
    user = create_user_in_db(database_connection)
    assert user.id is not None

@pytest.mark.slow
@pytest.mark.integration
def test_full_workflow():
    """Test requiring external services."""
    pass

# Run specific markers:
# pytest -m unit          # Run only unit tests
# pytest -m "not slow"    # Skip slow tests
# pytest -m "unit or integration"  # Run unit OR integration

Coverage Configuration

Ensure adequate test coverage:

# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_functions = ["test_*"]
addopts = [
    "--cov=src",                    # Measure coverage of src/ directory
    "--cov-report=term-missing",    # Show missing lines in terminal
    "--cov-report=html",            # Generate HTML coverage report
    "--cov-fail-under=80",          # Fail if coverage < 80%
    "--strict-markers",             # Require all markers to be registered
]
markers = [
    "unit: Unit tests",
    "integration: Integration tests",
    "slow: Slow tests",
]

# Run with coverage
# pytest --cov

Anti-Patterns to Avoid

❌ Tests Without Clear AAA Structure

# BAD: Mixed concerns
def bad_test():
    user = User.create({"email": "test@example.com"})
    assert user.email == "test@example.com"
    user.update({"username": "newname"})
    assert user.username == "newname"

# GOOD: Separate arrange/act/assert
def good_test():
    # ARRANGE
    user = User.create({"email": "test@example.com"})

    # ACT
    user.update({"username": "newname"})

    # ASSERT
    assert user.username == "newname"

❌ Overusing Broad Fixture Scopes

# BAD: Session-scoped mutable fixture
@pytest.fixture(scope="session")
def users():
    return []  # Shared across all tests - can cause failures

# GOOD: Function-scoped
@pytest.fixture(scope="function")
def users():
    return []  # Fresh for each test

❌ Conditional Logic in Parametrized Tests

# BAD
@pytest.mark.parametrize("value,expected", [(1, 2), (2, 4)])
def test_double(value, expected):
    result = double(value)
    if value == 1:
        assert result > 0
    else:
        assert result == expected

# GOOD: Split tests
@pytest.mark.parametrize("value,expected", [(1, 2), (2, 4)])
def test_double(value, expected):
    assert double(value) == expected

❌ Not Using Mocks for External Dependencies

# BAD: Real HTTP call in test
def test_api_client():
    client = APIClient()
    user = client.get_user(123)  # Real API call!
    assert user["id"] == 123

# GOOD: Mock external call
@patch('requests.get')
def test_api_client(mock_get):
    mock_get.return_value.json.return_value = {"id": 123}
    client = APIClient()
    user = client.get_user(123)
    assert user["id"] == 123

When to Use This Skill

Activate this skill when:

  • Writing new pytest test suites
  • Creating test fixtures or factories
  • Implementing parametrized tests
  • Testing async code
  • Organizing tests with conftest.py
  • Setting up test coverage requirements

Integration Points

This skill is a prerequisite for:

  • agentient-quality-assurance/advanced-testing-strategies - Extends these patterns
  • All plugins requiring 80%+ test coverage

Related Resources

For advanced testing strategies, see:

Skills Info
Original Name:pytest-patternsAuthor:agentient