maverick-python-testing
Python testing best practices with pytest, unittest, and mocking
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.
| name | maverick-python-testing |
|---|---|
| description | Python testing best practices with pytest, unittest, and mocking |
| version | 1.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_*.pyor*_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 functionclass- Per test classmodule- Per test modulesession- 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