Back to Skills

pytest-generator

matteocervelli
Updated Today
15 views
10
10
View on GitHub
Metatesting

About

This skill generates pytest-based unit tests for Python code, creating comprehensive test files that follow pytest conventions. It automatically produces tests with proper fixtures, mocking, and parametrization to add missing coverage or test new features. Use it when you need to quickly generate or expand pytest test suites for your Python modules.

Documentation

Pytest Generator Skill

Purpose

This skill generates pytest-based unit tests for Python code, following pytest conventions, best practices, and project standards. It creates comprehensive test suites with proper fixtures, mocking, parametrization, and coverage.

When to Use

  • Generate pytest tests for Python modules
  • Create test files for new Python features
  • Add missing test coverage to existing Python code
  • Need pytest-specific patterns (fixtures, markers, parametrize)

Test File Naming Convention

Source to Test Mapping:

  • Source: src/tools/feature/core.py
  • Test: tests/test_core.py
  • Pattern: test_<source_filename>.py

Examples:

  • src/utils/validator.pytests/test_validator.py
  • src/models/user.pytests/test_user.py
  • src/services/auth.pytests/test_auth.py

Pytest Test Generation Workflow

1. Analyze Python Source Code

Read the source file:

# Read the source to understand structure
cat src/tools/feature/core.py

Identify test targets:

  • Public functions to test
  • Classes and methods
  • Error conditions
  • Edge cases
  • Dependencies (imports, external calls)

Output: List of functions/classes requiring tests


2. Generate Test File Structure

Create test file with proper naming:

"""
Unit tests for [module name].

This module tests:
- [Functionality 1]
- [Functionality 2]
- Error handling and edge cases
"""

import pytest
from unittest.mock import Mock, MagicMock, patch, call
from typing import Any, Dict, List, Optional
from pathlib import Path

# Import functions/classes to test
from src.tools.feature.core import (
    function_to_test,
    ClassToTest,
    CustomException,
)


# ============================================================================
# Fixtures
# ============================================================================

@pytest.fixture
def sample_data() -> Dict[str, Any]:
    """
    Sample data for testing.

    Returns:
        Dictionary with test data
    """
    return {
        "id": 1,
        "name": "test",
        "value": 123,
    }


@pytest.fixture
def mock_dependency() -> Mock:
    """
    Mock external dependency.

    Returns:
        Configured mock object
    """
    mock = Mock()
    mock.method.return_value = {"status": "success"}
    mock.validate.return_value = True
    return mock


@pytest.fixture
def temp_directory(tmp_path: Path) -> Path:
    """
    Temporary directory for test files.

    Args:
        tmp_path: pytest temporary directory fixture

    Returns:
        Path to test directory
    """
    test_dir = tmp_path / "test_data"
    test_dir.mkdir()
    return test_dir


# ============================================================================
# Test Classes (for testing classes)
# ============================================================================

class TestClassName:
    """Tests for ClassName."""

    def test_init_valid_params_creates_instance(self):
        """Test initialization with valid parameters."""
        # Arrange & Act
        instance = ClassToTest(param="value")

        # Assert
        assert instance.param == "value"
        assert instance.initialized is True

    def test_method_valid_input_returns_expected(self, sample_data):
        """Test method with valid input."""
        # Arrange
        instance = ClassToTest()

        # Act
        result = instance.method(sample_data)

        # Assert
        assert result["processed"] is True
        assert result["id"] == sample_data["id"]

    def test_method_invalid_input_raises_error(self):
        """Test method with invalid input raises error."""
        # Arrange
        instance = ClassToTest()
        invalid_data = None

        # Act & Assert
        with pytest.raises(ValueError, match="Invalid input"):
            instance.method(invalid_data)


# ============================================================================
# Test Functions
# ============================================================================

def test_function_valid_input_returns_expected(sample_data):
    """Test function with valid input returns expected result."""
    # Arrange
    expected = "processed"

    # Act
    result = function_to_test(sample_data)

    # Assert
    assert result == expected


def test_function_empty_input_returns_empty():
    """Test function with empty input returns empty result."""
    # Arrange
    empty_input = {}

    # Act
    result = function_to_test(empty_input)

    # Assert
    assert result == {}


def test_function_none_input_raises_error():
    """Test function with None input raises ValueError."""
    # Arrange
    invalid_input = None

    # Act & Assert
    with pytest.raises(ValueError, match="Input cannot be None"):
        function_to_test(invalid_input)


def test_function_with_mock_dependency(mock_dependency):
    """Test function with mocked external dependency."""
    # Arrange
    input_data = {"key": "value"}

    # Act
    result = function_using_dependency(input_data, mock_dependency)

    # Assert
    assert result["status"] == "success"
    mock_dependency.method.assert_called_once_with(input_data)


@patch('src.tools.feature.core.external_api_call')
def test_function_with_patched_external(mock_api):
    """Test function with patched external API call."""
    # Arrange
    mock_api.return_value = {"data": "test"}
    input_data = {"key": "value"}

    # Act
    result = function_with_api(input_data)

    # Assert
    assert result["data"] == "test"
    mock_api.assert_called_once()


# ============================================================================
# Parametrized Tests
# ============================================================================

@pytest.mark.parametrize("input_value,expected", [
    ("[email protected]", True),
    ("invalid.email", False),
    ("", False),
    (None, False),
    ("no@domain", False),
    ("@no-user.com", False),
])
def test_validation_multiple_inputs(input_value, expected):
    """Test validation with multiple input scenarios."""
    # Act
    result = validate_input(input_value)

    # Assert
    assert result == expected


@pytest.mark.parametrize("user_type,permission", [
    ("admin", "all"),
    ("moderator", "edit"),
    ("user", "read"),
    ("guest", "none"),
])
def test_permissions_by_user_type(user_type, permission):
    """Test permissions based on user type."""
    # Arrange
    user = {"type": user_type}

    # Act
    result = get_permissions(user)

    # Assert
    assert result == permission


# ============================================================================
# Async Tests
# ============================================================================

@pytest.mark.asyncio
async def test_async_function_success():
    """Test async function with successful execution."""
    # Arrange
    input_data = {"key": "value"}

    # Act
    result = await async_function(input_data)

    # Assert
    assert result.success is True
    assert result.data == input_data


@pytest.mark.asyncio
async def test_async_function_with_mock():
    """Test async function with mocked dependency."""
    # Arrange
    mock_service = Mock()
    mock_service.fetch = AsyncMock(return_value={"data": "test"})
    input_data = {"key": "value"}

    # Act
    result = await async_function_with_service(input_data, mock_service)

    # Assert
    assert result["data"] == "test"
    mock_service.fetch.assert_awaited_once()


# ============================================================================
# Exception Tests
# ============================================================================

def test_custom_exception_raised():
    """Test that custom exception is raised."""
    # Arrange
    invalid_input = "invalid"

    # Act & Assert
    with pytest.raises(CustomException):
        function_that_raises(invalid_input)


def test_exception_message_content():
    """Test exception message contains expected content."""
    # Arrange
    invalid_input = "invalid"

    # Act & Assert
    with pytest.raises(CustomException, match="Expected error message"):
        function_that_raises(invalid_input)


def test_exception_attributes():
    """Test exception has expected attributes."""
    # Arrange
    invalid_input = "invalid"

    # Act & Assert
    with pytest.raises(CustomException) as exc_info:
        function_that_raises(invalid_input)

    assert exc_info.value.code == 400
    assert "field" in exc_info.value.details


# ============================================================================
# File Operation Tests
# ============================================================================

def test_save_file(temp_directory):
    """Test file saving functionality."""
    # Arrange
    file_path = temp_directory / "test_file.txt"
    content = "test content"

    # Act
    save_file(file_path, content)

    # Assert
    assert file_path.exists()
    assert file_path.read_text() == content


def test_read_file(temp_directory):
    """Test file reading functionality."""
    # Arrange
    file_path = temp_directory / "test_file.txt"
    expected_content = "test content"
    file_path.write_text(expected_content)

    # Act
    content = read_file(file_path)

    # Assert
    assert content == expected_content


def test_file_not_found_raises_error(temp_directory):
    """Test reading non-existent file raises error."""
    # Arrange
    missing_file = temp_directory / "missing.txt"

    # Act & Assert
    with pytest.raises(FileNotFoundError):
        read_file(missing_file)


# ============================================================================
# Marker Examples
# ============================================================================

@pytest.mark.slow
def test_slow_operation():
    """Test slow operation (marked as slow)."""
    # This test can be skipped with: pytest -m "not slow"
    pass


@pytest.mark.integration
def test_integration_scenario():
    """Test integration scenario (marked as integration)."""
    # Run only integration tests: pytest -m integration
    pass


@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9+")
def test_python39_feature():
    """Test feature that requires Python 3.9+."""
    pass


# ============================================================================
# Fixture Scope Examples
# ============================================================================

@pytest.fixture(scope="module")
def expensive_setup():
    """
    Expensive setup that runs once per module.

    Returns:
        Setup result
    """
    # Setup runs once for entire test module
    result = perform_expensive_setup()
    yield result
    # Teardown runs once after all tests
    cleanup(result)


@pytest.fixture(scope="function")
def per_test_setup():
    """
    Setup that runs before each test function.

    Returns:
        Setup result
    """
    # Setup runs before each test
    result = setup()
    yield result
    # Teardown runs after each test
    teardown(result)

Deliverable: Complete pytest test file


Pytest-Specific Patterns

1. Fixtures

Basic fixture:

@pytest.fixture
def sample_user():
    """Create sample user for testing."""
    return User(name="Test User", email="[email protected]")

Fixture with setup and teardown:

@pytest.fixture
def database_connection():
    """Database connection with cleanup."""
    # Setup
    conn = connect_to_database()

    yield conn  # Test uses connection here

    # Teardown
    conn.close()

Fixture with parameters:

@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database_type(request):
    """Parametrized database fixture."""
    return request.param


def test_with_all_databases(database_type):
    """Test runs 3 times, once per database."""
    db = connect(database_type)
    assert db.connected

Fixture scopes:

@pytest.fixture(scope="function")  # Default: runs per test
def per_test():
    pass


@pytest.fixture(scope="class")  # Runs once per test class
def per_class():
    pass


@pytest.fixture(scope="module")  # Runs once per module
def per_module():
    pass


@pytest.fixture(scope="session")  # Runs once per session
def per_session():
    pass

2. Parametrize

Basic parametrization:

@pytest.mark.parametrize("input,expected", [
    (2, 4),
    (3, 9),
    (4, 16),
])
def test_square(input, expected):
    assert square(input) == expected

Multiple parameters:

@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (2, 3, 5),
    (10, 20, 30),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

Named parameters:

@pytest.mark.parametrize("test_input,expected", [
    pytest.param("valid", True, id="valid_input"),
    pytest.param("invalid", False, id="invalid_input"),
    pytest.param("", False, id="empty_input"),
])
def test_validation(test_input, expected):
    assert validate(test_input) == expected

3. Markers

Built-in markers:

@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
    pass


@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
def test_unix_feature():
    pass


@pytest.mark.xfail(reason="Known bug #123")
def test_buggy_feature():
    pass


@pytest.mark.slow
def test_slow_operation():
    pass

Custom markers (in pytest.ini):

[pytest]
markers =
    slow: marks tests as slow
    integration: marks tests as integration tests
    unit: marks tests as unit tests
    smoke: marks tests as smoke tests

4. Mocking with pytest

Mock with pytest-mock:

def test_with_mocker(mocker):
    """Test using pytest-mock plugin."""
    mock_api = mocker.patch('module.api_call')
    mock_api.return_value = {"status": "success"}

    result = function_using_api()

    assert result["status"] == "success"
    mock_api.assert_called_once()

Mock attributes:

def test_mock_attributes(mocker):
    """Test with mocked object attributes."""
    mock_obj = mocker.Mock()
    mock_obj.property = "value"
    mock_obj.method.return_value = 42

    assert mock_obj.property == "value"
    assert mock_obj.method() == 42

5. Async Testing

Async test:

@pytest.mark.asyncio
async def test_async_function():
    """Test async function."""
    result = await async_function()
    assert result.success


@pytest.mark.asyncio
async def test_async_with_mock(mocker):
    """Test async with mocked async call."""
    mock_service = mocker.Mock()
    mock_service.fetch = AsyncMock(return_value="data")

    result = await function_with_async_call(mock_service)

    assert result == "data"
    mock_service.fetch.assert_awaited_once()

Test Generation Strategy

For Functions

  1. Happy path test: Normal successful execution
  2. Edge case tests: Empty input, max values, min values
  3. Error tests: Invalid input, None values
  4. Dependency tests: Mock external dependencies

For Classes

  1. Initialization tests: Valid params, invalid params
  2. Method tests: Each public method
  3. State tests: Verify state changes
  4. Property tests: Getters and setters
  5. Error tests: Exception handling

For Modules

  1. Import tests: Module can be imported
  2. Public API tests: All public functions/classes
  3. Integration tests: Module interactions
  4. Configuration tests: Config loading and validation

Running Pytest

Basic commands:

# Run all tests
pytest

# Run specific file
pytest tests/test_module.py

# Run specific test
pytest tests/test_module.py::test_function

# Run with verbose output
pytest -v

# Run with coverage
pytest --cov=src --cov-report=html --cov-report=term-missing

# Run with markers
pytest -m "not slow"
pytest -m integration

# Run in parallel (with pytest-xdist)
pytest -n auto

# Run with output
pytest -s  # Show print statements
pytest -v -s  # Verbose + output

Coverage commands:

# Generate coverage report
pytest --cov=src --cov-report=html

# View HTML report
open htmlcov/index.html

# Check coverage threshold
pytest --cov=src --cov-fail-under=80

# Show missing lines
pytest --cov=src --cov-report=term-missing

Best Practices

  1. Use descriptive test names: test_function_condition_expected_result
  2. Follow AAA pattern: Arrange, Act, Assert
  3. One assertion per test (generally)
  4. Use fixtures for setup: Reusable test setup
  5. Mock external dependencies: Isolate unit under test
  6. Parametrize similar tests: Reduce code duplication
  7. Use markers for organization: Group related tests
  8. Keep tests independent: No test depends on another
  9. Test edge cases: Empty, None, max values
  10. Test error conditions: Exceptions and failures

Quality Checklist

Before marking tests complete:

  • Test file properly named (test_<module>.py)
  • All public functions/classes tested
  • Happy path tests included
  • Edge case tests included
  • Error condition tests included
  • External dependencies mocked
  • Fixtures used for reusable setup
  • Tests follow AAA pattern
  • Test names are descriptive
  • All tests pass
  • Coverage ≥ 80%
  • No flaky tests
  • Tests run quickly

Integration with Testing Workflow

Input: Python source file to test Process: Analyze → Generate structure → Write tests → Run & verify Output: pytest test file with ≥ 80% coverage Next Step: Integration testing or code review


Remember

  • Follow naming convention: test_<source_file>.py
  • Use pytest fixtures for reusable setup
  • Parametrize to reduce duplication
  • Mock external calls to isolate tests
  • Test behavior, not implementation
  • Aim for 80%+ coverage
  • Keep tests fast and independent

Quick Install

/plugin add https://github.com/matteocervelli/llms/tree/main/pytest-generator

Copy and paste this command in Claude Code to install this skill

GitHub 仓库

matteocervelli/llms
Path: .claude/skills/pytest-generator

Related Skills