maverick-python-testing

get2knowio's avatarfrom get2knowio

Python testing best practices with pytest, unittest, and mocking

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 7, 2026⬇️3downloads

When & Why to Use This Skill

This Claude skill provides expert guidance on Python testing best practices, focusing on pytest, unittest, and advanced mocking patterns. It empowers developers to build robust, maintainable test suites through optimized fixture management, asynchronous test handling, and comprehensive code coverage strategies to ensure high-quality software delivery.

Use Cases

  • Architecting scalable and reusable test environments using pytest fixtures and shared configurations in conftest.py.
  • Isolating units of code by effectively mocking external dependencies and third-party APIs using unittest.mock and patch decorators.
  • Implementing data-driven testing with pytest parametrization to validate multiple edge cases and input combinations efficiently.
  • Measuring and improving software reliability by identifying untested critical code paths through integrated coverage reporting.
  • Refactoring legacy test suites to eliminate common antipatterns, such as testing implementation details or creating test interdependencies.
namemaverick-python-testing
descriptionPython testing best practices with pytest, unittest, and mocking
version1.0.0

Python Testing Skill

Expert guidance for Python testing with pytest, unittest, fixtures, and mocking patterns.

Pytest Best Practices

Test Structure

  • Name tests test_*.py or *_test.py
  • Use fixtures for test setup (avoid setUp/tearDown)
  • Group related tests in classes (optional, for organization)
  • One assertion per test (or closely related assertions)

Example:

import pytest

@pytest.fixture
def sample_data():
    """Fixture providing test data."""
    return {"key": "value"}

def test_function_with_fixture(sample_data):
    """Test using fixture data."""
    result = process(sample_data)
    assert result == expected_value

Fixtures

Scope Levels:

  • function (default) - Per test function
  • class - Per test class
  • module - Per test module
  • session - Per test session

Example:

@pytest.fixture(scope="module")
def database():
    """Module-scoped database fixture."""
    db = Database()
    db.connect()
    yield db
    db.disconnect()

@pytest.fixture
def user(database):
    """Function-scoped user (depends on database)."""
    user = database.create_user("test@example.com")
    yield user
    database.delete_user(user.id)

Parametrize for Multiple Cases

Single Parameter:

@pytest.mark.parametrize("input,expected", [
    (1, 2),
    (2, 4),
    (3, 6),
])
def test_double(input, expected):
    assert double(input) == expected

Multiple Parameters:

@pytest.mark.parametrize("username", ["alice", "bob"])
@pytest.mark.parametrize("role", ["admin", "user"])
def test_permissions(username, role):
    """Runs 4 tests (2x2 combinations)."""
    user = User(username, role)
    assert user.has_role(role)

Async Tests

import pytest

@pytest.mark.asyncio
async def test_async_function():
    result = await async_operation()
    assert result == expected

Mocking Patterns

unittest.mock

Patch Decorator:

from unittest.mock import patch, MagicMock

@patch('module.external_api_call')
def test_with_mock(mock_api):
    mock_api.return_value = {"status": "ok"}

    result = function_that_calls_api()

    mock_api.assert_called_once_with(expected_args)
    assert result == expected

Patch Context Manager:

def test_with_context_manager():
    with patch('module.function') as mock_func:
        mock_func.return_value = 42
        assert call_function() == 42

Mock Attributes:

mock = MagicMock()
mock.method.return_value = "result"
mock.attribute = "value"

# Assertions
mock.method.assert_called()
mock.method.assert_called_with(arg1, arg2)
mock.method.assert_called_once()
assert mock.method.call_count == 3

Common Mocking Mistakes

WRONG - Patching in wrong place:

# File: myapp/module.py
from external import api_call

def my_function():
    return api_call()

# File: tests/test_module.py
# WRONG - patches external.api_call, not myapp.module.api_call
@patch('external.api_call')
def test_wrong(mock_api):
    my_function()  # Doesn't use the mock!

CORRECT:

# CORRECT - patch where it's used, not where it's defined
@patch('myapp.module.api_call')
def test_correct(mock_api):
    my_function()  # Uses the mock

Test Coverage

Aim for Meaningful Coverage

  • Target: >80% coverage for critical code
  • Test edge cases (empty lists, None, zero, negative numbers)
  • Test error conditions (exceptions, invalid input)
  • Don't test framework code or trivial getters/setters

Run Coverage

pytest --cov=myapp --cov-report=html tests/

Coverage Configuration

# pytest.ini or pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "--cov=src --cov-report=term-missing"

Test Organization

Directory Structure

project/
├── src/
│   └── myapp/
│       ├── __init__.py
│       └── module.py
└── tests/
    ├── conftest.py           # Shared fixtures
    ├── unit/
    │   └── test_module.py
    └── integration/
        └── test_workflow.py

conftest.py for Shared Fixtures

# tests/conftest.py
import pytest

@pytest.fixture(scope="session")
def app_config():
    """Application configuration for all tests."""
    return {"debug": True, "db": ":memory:"}

@pytest.fixture
def temp_file(tmp_path):
    """Create a temporary file for testing."""
    file = tmp_path / "test.txt"
    file.write_text("test content")
    return file

Common Testing Patterns

Testing Exceptions

import pytest

def test_raises_exception():
    with pytest.raises(ValueError, match="invalid input"):
        function_that_raises("bad")

Testing Warnings

import warnings
import pytest

def test_warning():
    with pytest.warns(DeprecationWarning):
        legacy_function()

Testing Async Generators

@pytest.mark.asyncio
async def test_async_generator():
    gen = async_generator()
    items = [item async for item in gen]
    assert len(items) == 3

Testing with Temporary Files

def test_file_processing(tmp_path):
    """pytest provides tmp_path fixture."""
    test_file = tmp_path / "input.txt"
    test_file.write_text("test data")

    process_file(test_file)

    output = (tmp_path / "output.txt").read_text()
    assert output == "processed: test data"

Markers for Test Organization

import pytest

@pytest.mark.slow
def test_slow_operation():
    """Mark slow tests to skip in quick runs."""
    pass

@pytest.mark.integration
def test_database_integration():
    """Mark integration tests separately."""
    pass

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

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

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

Run specific markers:

pytest -m "not slow"           # Skip slow tests
pytest -m integration          # Only integration tests

Antipatterns to Avoid

❌ Testing Implementation Details

# BAD - tests internal implementation
def test_cache_uses_dict():
    cache = Cache()
    assert isinstance(cache._storage, dict)

# GOOD - tests behavior
def test_cache_retrieval():
    cache = Cache()
    cache.set("key", "value")
    assert cache.get("key") == "value"

❌ Unclear Test Names

# BAD
def test_user():
    pass

# GOOD
def test_user_creation_with_valid_email():
    pass

❌ Multiple Unrelated Assertions

# BAD - tests multiple unrelated things
def test_everything():
    assert func1() == 1
    assert func2() == 2
    assert func3() == 3

# GOOD - separate tests
def test_func1_returns_one():
    assert func1() == 1

def test_func2_returns_two():
    assert func2() == 2

❌ Test Interdependencies

# BAD - tests depend on execution order
def test_create_user():
    global user_id
    user_id = create_user("alice")

def test_get_user():
    user = get_user(user_id)  # Depends on previous test!

# GOOD - use fixtures for setup
@pytest.fixture
def user_id():
    return create_user("alice")

def test_get_user(user_id):
    user = get_user(user_id)

Review Severity Guidelines

  • CRITICAL: No tests for new code, tests disabled/skipped without justification
  • MAJOR: Missing edge case tests, testing implementation details, test interdependencies
  • MINOR: Missing docstrings on tests, inconsistent naming
  • SUGGESTION: Could use parametrize for similar tests, opportunity for shared fixture

References