From pytest-forge
This skill should be used when the user asks to "write tests", "create tests", "generate tests", "unit tests", "pytest", "test guidelines", "testing rules", "test standards", "how to test", "test this code", "add tests", "validate tests", "check tests", "improve tests", or when working with Python test files. Provides comprehensive guidelines for creating pytest unit tests with 100% coverage following strict naming conventions, mock patterns, and parametrization standards.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pytest-forge:pytest-guidelinesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
These guidelines define a rigorous testing approach for Python projects using pytest, emphasizing 100% code coverage, strict naming conventions,
These guidelines define a rigorous testing approach for Python projects using pytest, emphasizing 100% code coverage, strict naming conventions, comprehensive mock verification, and maintainable test structure. Follow these guidelines to create tests that ensure code reliability and prevent regressions.
100% Coverage Requirement: Every line of code must be tested. This ensures that any code change will not have negative impact on untested code.
Comprehensive Mock Verification: All mock interactions must be verified through the mock_calls property. This ensures that all manipulations of
mocks are checked, preventing incomplete test coverage of external dependencies.
Consistent Naming: Strict naming conventions improve test readability and maintenance, making it immediately clear what each test covers.
Test Order Matches Source: Tests should appear in the same order as methods in the source file, making it easy to verify complete coverage by visual inspection.
MANDATORY PRE-CHECK: Before generating tests, improving tests, or validating tests, the commands generate-tests, improve-tests, and
validate-tests must first verify that the source code has complete type annotations.
Required type annotations:
Example of properly typed code:
class DataProcessor:
def __init__(self, config: dict[str, str]) -> None:
self.config = config
def process(self, data: list[str], validate: bool = True) -> dict[str, int]:
# Implementation
pass
def _validate(self, item: str) -> bool:
# Implementation
pass
Example of code missing type annotations (will be rejected):
class DataProcessor:
def __init__(self, config): # Missing parameter and return types
self.config = config
def process(self, data, validate=True): # Missing all type annotations
pass
def _validate(self, item): # Missing parameter and return types
pass
Validation process:
self and cls)-> None for void functions)Why this matters:
Mirror the source file structure in the tests/ directory:
src/utils/parser.py → tests/utils/test_parser.py
src/models/user.py → tests/models/test_user.py
Each test file name must start with test_ prefix.
Follow this pattern: test_<method_name> for single-scenario tests, or test_<method_name>__<case_description> for multiple scenarios.
Basic format:
def test_calculate_total():
# Tests the calculate_total method
Include underscore prefix for private methods:
# For method named _private_helper
def test__private_helper():
# Underscore is included in test name
Multiple test cases:
def test_validate_input__valid_data():
# Test with valid data
def test_validate_input__invalid_format():
# Test with invalid format
def test_validate_input__empty_input():
# Test with empty input
The double underscore __ separates the method name from the case description.
Use these standard names consistently across all tests:
tested: The class or instance being tested (MUST be assigned FIRST in every test)result: The value returned from the method under test (MUST be used for return values)expected: The expected value for single assertions (MUST be used when comparing result)exp_*: Expected values for multiple assertions (e.g., exp_calls, exp_output, exp_status)CRITICAL - Variable Naming Requirements:
Every test MUST follow these naming conventions strictly:
tested Variabletested variable FIRSTtested, NEVER directly on the class nameExample for instance methods:
def test_format_name():
tested = NameFormatter() # Assign instance to tested
result = tested.format_name("john", "doe") # Call on tested
expected = "John Doe"
assert result == expected
Example for class/static methods:
def test_parse_arguments():
tested = SvgToPngConverter # Assign CLASS to tested (not an instance)
result = tested.parse_arguments() # Call on tested
expected = ConversionInput(...)
assert result == expected
FORBIDDEN:
# WRONG - calling directly on class name
def test_parse_arguments():
result = SvgToPngConverter.parse_arguments() # FORBIDDEN
...
result Variableresultoutput, ret, actual, response, value, etc.CORRECT:
def test_calculate_total():
tested = Calculator()
result = tested.calculate_total([1, 2, 3]) # CORRECT: use 'result'
expected = 6
assert result == expected
FORBIDDEN:
def test_calculate_total():
tested = Calculator()
output = tested.calculate_total([1, 2, 3]) # FORBIDDEN: don't use 'output'
total = tested.calculate_total([1, 2, 3]) # FORBIDDEN: don't use 'total'
ret = tested.calculate_total([1, 2, 3]) # FORBIDDEN: don't use 'ret'
...
expected Variableexpectedwant, exp, correct, answer, etc.CORRECT:
def test_format_date():
tested = DateFormatter()
result = tested.format("2024-01-15")
expected = "January 15, 2024" # CORRECT: use 'expected'
assert result == expected
FORBIDDEN:
def test_format_date():
tested = DateFormatter()
result = tested.format("2024-01-15")
assert result == "January 15, 2024" # FORBIDDEN: inlined expected value
def test_format_date():
tested = DateFormatter()
result = tested.format("2024-01-15")
want = "January 15, 2024" # FORBIDDEN: don't use 'want'
assert result == want
exp_* Pattern for Multiple Assertionsexp_ prefix for expected values when a test has multiple assertionsexp_calls, exp_output, exp_status, exp_error, exp_mock_callsexpected_ prefix (too long) - always use exp_CORRECT:
@patch('module.api')
def test_fetch_data(mock_api):
mock_api.get.side_effect = [{"data": "value"}]
tested = DataFetcher()
result = tested.fetch("http://example.com")
expected = {"data": "value"}
assert result == expected
exp_api_calls = [call.get("http://example.com")] # CORRECT: use 'exp_' prefix
assert mock_api.mock_calls == exp_api_calls
FORBIDDEN:
@patch('module.api')
def test_fetch_data(mock_api):
...
expected_calls = [call.get("http://example.com")] # FORBIDDEN: don't use 'expected_'
expected_api_calls = [call.get("http://example.com")] # FORBIDDEN: too long
calls = [call.get("http://example.com")] # FORBIDDEN: not descriptive
...
For detailed naming examples, see references/naming-conventions.md.
Singleton Comparisons: Use is (not ==) when comparing to singletons: True, False, None.
Correct:
def test_is_valid():
tested = Validator()
result = tested.is_valid("data")
expected = True
assert result is expected # Use 'is' for boolean singletons
Incorrect:
def test_is_valid():
tested = Validator()
result = tested.is_valid("data")
expected = True
assert result == expected # Wrong: use 'is' not '=='
When to use is:
TrueFalseNoneWhen to use ==:
Tests must appear in the same order as methods appear in the source file. This enables:
Source file order:
class Calculator:
def __init__(self):
pass
def add(self, a, b):
pass
def subtract(self, a, b):
pass
def _validate(self, value):
pass
Test file order:
def test_add():
pass
def test_subtract():
pass
def test__validate():
pass
MANDATORY: When testing a class that inherits from a base class, the very first test in the test file must verify the inheritance relationship
using issubclass().
This test ensures that:
Example:
# Source: scripts/user_input_logger.py
class UserInputsLogger(BaseLogger):
def process(self, data):
pass
# Test file: tests/scripts/test_user_input_logger.py
def test_inheritance():
"""Verify UserInputsLogger inherits from BaseLogger."""
assert issubclass(UserInputsLogger, BaseLogger)
def test_process():
# Remaining tests follow...
pass
Pattern:
def test_inheritance():
"""Verify <ClassName> inherits from <BaseClassName>."""
assert issubclass(ClassName, BaseClassName)
This test should:
test_inheritanceassert issubclass(DerivedClass, BaseClass) statementMANDATORY: When testing a NamedTuple class, the first test must verify the NamedTuple structure using the is_namedtuple() helper function.
The test must:
test_class (not test_inheritance)tested variable (not an instance)fields dictionary with field names and their typesis_namedtuple() helper function to verify the structureSetup - Add helper to conftest.py:
Add this function to your tests/conftest.py file (create if it doesn't exist). This makes the helper available to all test files automatically:
# tests/conftest.py
from typing import get_type_hints
def is_namedtuple(cls, fields: dict) -> bool:
"""
Verify that a class is a NamedTuple with the expected fields and types.
Args:
cls: The class to check
fields: Dictionary mapping field names to their expected types
Returns:
bool: True if cls is a NamedTuple with exactly the specified fields and types
"""
return (
issubclass(cls, tuple)
and hasattr(cls, "_fields")
and isinstance(cls._fields, tuple)
and len([field for field in cls._fields if field in fields]) == len(fields.keys())
and get_type_hints(cls) == fields
)
Example usage in tests:
# Source: models/validation_result.py
from typing import NamedTuple
class ValidationResult(NamedTuple):
has_errors: bool
errors: list[str]
# Test file: tests/models/test_validation_result.py
from validation_result import ValidationResult
# Note: is_namedtuple is automatically available from conftest.py
def test_class():
"""Verify ValidationResult is a NamedTuple with correct fields."""
tested = ValidationResult
fields = {"has_errors": bool, "errors": list[str]}
assert is_namedtuple(tested, fields)
Why use is_namedtuple() instead of issubclass():
_fields attribute exists and is correctget_type_hints()For a complete NamedTuple testing example, see examples/namedtuple-test-file.py.
MANDATORY: When testing a dataclass, the first test must verify the dataclass structure using the is_dataclass() helper function.
The test must:
test_class (not test_inheritance)tested variable (not an instance)fields dictionary with field names and their type stringsis_dataclass() helper function to verify the structureSetup - Add helper to conftest.py:
Add this function to your tests/conftest.py file alongside is_namedtuple():
# tests/conftest.py
from dataclasses import fields as dataclass_fields, is_dataclass as dataclass_is_dataclass
def is_dataclass(cls, fields: dict) -> bool:
"""
Verify that a class is a dataclass with the expected fields and types.
Args:
cls: The class to check
fields: Dictionary mapping field names to their expected types (can be type objects or strings)
Returns:
bool: True if cls is a dataclass with exactly the specified fields and types
"""
if not dataclass_is_dataclass(cls):
return False
actual_fields = dataclass_fields(cls)
if len([field for field in actual_fields if field.name in fields]) != len(fields.keys()):
return False
for field in actual_fields:
expected_type = fields[field.name]
actual_type = field.type
if expected_type != actual_type:
return False
return True
Example usage in tests:
# Source: models/transcript_segment.py
from dataclasses import dataclass
@dataclass
class TranscriptSegment:
speaker: str
text: str
chunk: int
start: float
end: float
# Test file: tests/models/test_transcript_segment.py
from transcript_segment import TranscriptSegment
# Note: is_dataclass is automatically available from conftest.py
def test_class():
"""Verify TranscriptSegment is a dataclass with correct fields."""
tested = TranscriptSegment
fields = {
"speaker": "str",
"text": "str",
"chunk": "int",
"start": "float",
"end": "float",
}
assert is_dataclass(tested, fields)
Why use is_dataclass() helper:
@dataclass__dataclass_fields__ manuallyFor a complete dataclass testing example, see examples/dataclass-test-file.py.
Every method must have at least one test. If a method requires multiple test scenarios, prefer parametrization (see below) or create multiple test functions with case suffixes.
__main__ Blocks from TestsIMPORTANT: Do NOT write tests for if __name__ == "__main__": blocks.
These blocks are:
pyproject.toml via exclude_lines)exec(compile(...))FORBIDDEN:
class TestMainBlock:
"""Tests for the __main__ block execution."""
def test_main_runs(self) -> None:
# FORBIDDEN: Don't test __main__ blocks
exec(compile('if __name__ == "__main__": ...', "<string>", "exec"), ...)
Why this matters:
__main__ block typically just calls ClassName.run() which should already have testsside_effect for Return ValuesMANDATORY: When configuring mock return values, use side_effect instead of return_value. This is a strict requirement and must never be
violated. For call chains, only the final call in the chain must use side_effect (for example:
mock.return_value.get.return_value.add.side_effect = [...]).
Correct:
@patch('module.api_client')
def test_fetch_data(mock_client):
mock_client.return_value.get.side_effect = [{"status": "success"}]
Incorrect - FORBIDDEN:
@patch('module.api_client')
def test_fetch_data(mock_client):
mock_client.return_value.get.return_value = {"status": "success"} # NEVER use return_value for the final call
For complex return objects (like HTTP responses), use SimpleNamespace instead of MagicMock:
CRITICAL: When a mock returns an object with attributes or methods, use SimpleNamespace NOT MagicMock. This avoids the need to verify
mock_calls on the returned object.
CORRECT - Use SimpleNamespace for response objects:
from types import SimpleNamespace
@patch('module.requests.post')
def test_api_call(mock_post):
# Use SimpleNamespace - no need to verify mock_calls on response
mock_post.side_effect = [
SimpleNamespace(
status_code=200,
text="response text",
json=lambda: {"data": "value"} # Lambda for methods that return values
)
]
tested = ApiClient()
result = tested.call_api()
expected = {"data": "value"}
assert result == expected
# Only need to verify mock_post, NOT the response object
exp_post_calls = [call("https://api.example.com")]
assert mock_post.mock_calls == exp_post_calls
FORBIDDEN - Using MagicMock for response objects:
@patch('module.requests.post')
def test_api_call(mock_post):
# FORBIDDEN: Using MagicMock for response requires mock_calls verification
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.side_effect = [{"data": "value"}]
mock_post.side_effect = [mock_response]
result = tested.call_api()
# If you use MagicMock, you MUST verify its mock_calls - easy to forget!
exp_response_calls = [call.json()]
assert mock_response.mock_calls == exp_response_calls # Often forgotten!
Why SimpleNamespace is preferred:
mock_calls verification needed - it's just a data containerjson=lambda: {...}, read=lambda: b"data"capsys Instead of Mocking printCRITICAL: When testing code that uses print(), use pytest's built-in capsys fixture instead of mocking print.
FORBIDDEN - Mocking print:
@patch("module.print")
def test_output(mock_print):
tested = MyClass()
tested.display_message()
# Complex extraction of print calls - error prone!
print_calls = [c[0][0] for c in mock_print.call_args_list if c[0]]
assert "Expected message" in print_calls
# Must also verify mock_calls - easy to forget!
exp_print_calls = [call("Expected message")]
assert mock_print.mock_calls == exp_print_calls
CORRECT - Using capsys fixture:
def test_output(self, capsys) -> None:
tested = MyClass()
tested.display_message()
captured = capsys.readouterr()
assert "Expected message" in captured.out
# Or for exact match:
expected = "Expected message\n"
assert captured.out == expected
Why capsys is preferred:
captured.out or captured.errcapsys usage patterns:
def test_stdout(self, capsys) -> None:
print("Hello")
captured = capsys.readouterr()
assert captured.out == "Hello\n"
def test_stderr(self, capsys) -> None:
import sys
print("Error", file=sys.stderr)
captured = capsys.readouterr()
assert captured.err == "Error\n"
def test_multiple_prints(self, capsys) -> None:
print("Line 1")
print("Line 2")
captured = capsys.readouterr()
assert "Line 1" in captured.out
assert "Line 2" in captured.out
mock_callsMANDATORY: After each test, ALL mock objects must be verified through the mock_calls property. This includes:
mock_db)MagicMock() objects you create (e.g., mock_response).return_value mock instances (e.g., mock_db_class.return_value)CRITICAL - Parametrized Tests Are NOT Exempt:
Parametrized tests MUST verify all mocks just like regular tests. If a test has mock parameters, each mock MUST have a corresponding
assert mock.mock_calls == exp_* statement.
FORBIDDEN - Parametrized test without mock verification:
@pytest.mark.parametrize("input_val,expected", [...])
@patch("module.api")
def test_fetch(mock_api, input_val, expected):
mock_api.get.side_effect = [{"data": input_val}]
tested = Fetcher()
result = tested.fetch(input_val)
assert result == expected
# FORBIDDEN: mock_api is NEVER verified with mock_calls!
CORRECT - Parametrized test with mock verification:
@pytest.mark.parametrize("input_val,expected", [...])
@patch("module.api")
def test_fetch(mock_api, input_val, expected):
mock_api.get.side_effect = [{"data": input_val}]
tested = Fetcher()
result = tested.fetch(input_val)
expected_result = expected
assert result == expected_result
exp_api_calls = [call.get(input_val)] # MUST verify mock
assert mock_api.mock_calls == exp_api_calls
When to avoid parametrization with mocks: If mock verification differs significantly between test cases, use separate non-parametrized tests instead of trying to parametrize with complex mock verification logic.
CRITICAL: Always verify at the mock object level, NOT at individual method level:
assert mock_response.mock_calls == exp_calls (object level)assert mock_response.read.mock_calls == exp_calls (method level)CRITICAL: Use single assertion with hard-coded values:
assert mock.mock_calls == exp_calls (single assertion)assert len(mock.mock_calls) == 3 then checking individual itemscall(f"URL: {tested_url}")call("URL: http://example.com")Correct verification:
Using SimpleNamespace:
@patch('module.requests.get')
def test_fetch_data(mock_get):
# Create mock response object
mock_get.side_effect = [SimpleNamespace(status_code=200, json=lambda: {"data": "value"})]
tested = APIClient()
result = tested.fetch_data("http://api.example.com/data")
expected = {"data": "value"}
assert result == expected
# CRITICAL: Verify with single assertion and HARD-CODED values
exp_get_calls = [call("http://api.example.com/data")]
assert mock_get.mock_calls == exp_get_calls
Using embedded mocks:
@patch('module.requests.get')
def test_fetch_data(mock_get):
# Create mock response object
mock_response = MagicMock()
mock_response.json.side_effect = [{"data": "value"}]
mock_get.side_effect = [mock_response]
tested = APIClient()
result = tested.fetch_data("http://api.example.com/data")
expected = {"data": "value"}
assert result == expected
# CRITICAL: Verify with single assertion and HARD-CODED values
exp_get_calls = [call("http://api.example.com/data")]
assert mock_get.mock_calls == exp_get_calls
# CRITICAL: Verify response mock at OBJECT level with hard-coded values
exp_response_calls = [call.json()]
assert mock_response.mock_calls == exp_response_calls # Single assertion!
Common mistakes:
# WRONG - mock_response is not verified!
@patch('module.requests.get')
def test_fetch_data(mock_get):
mock_response = MagicMock()
mock_response.json.side_effect = [{"data": "value"}]
mock_get.side_effect = [mock_response]
result = tested.fetch_data("url")
# FORBIDDEN: Only verifying mock_get, missing mock_response!
assert mock_get.mock_calls == [call("url")]
# WRONG - verifying individual methods instead of object!
@patch('module.urlopen')
def test_fetch(mock_urlopen):
mock_response = MagicMock()
mock_response.read.side_effect = [b"data"]
mock_urlopen.side_effect = [mock_response]
result = fetch("url")
# FORBIDDEN: Verifying at method level!
assert mock_response.read.mock_calls == [call()] # WRONG LEVEL
# CORRECT: Verify at object level
exp_response_calls = [call.read()]
assert mock_response.mock_calls == exp_response_calls # Object level!
# WRONG - checking length then individual calls with variables!
@patch('builtins.print')
def test_process(mock_print):
tested_url = "http://example.com"
process(tested_url)
# FORBIDDEN: Checking length and individual items
assert len(mock_print.mock_calls) == 2
assert mock_print.mock_calls[0] == call(f"Processing: {tested_url}")
assert mock_print.mock_calls[1] == call("Done")
# CORRECT: Single assertion with hard-coded values
exp_print_calls = [
call("Processing: http://example.com"), # Hard-coded!
call("Done")
]
assert mock_print.mock_calls == exp_print_calls
NEVER USE these assertion helper methods:
mock.assert_called()mock.assert_called_once()mock.assert_called_with(...)mock.assert_called_once_with(...)mock.assert_not_called()mock.assert_any_call(...)mock.assert_has_calls(...)mock.call_countmock.call_argsmock.call_args_listNEVER USE ANY from unittest.mock in mock_calls assertions or expected values:
ANY is a lazy matcher that defeats the purpose of precise mock verificationcall.method(ANY), construct the exact expected argumentFORBIDDEN:
from unittest.mock import ANY
# FORBIDDEN: ANY hides what the actual argument should be
assert mock_client.mock_calls == [call.send(ANY)]
# FORBIDDEN: ANY in expected values
assert result == ANY
CORRECT — mock the non-deterministic source and construct exact expected values:
@patch("module.Email.now") # Mock the timestamp source
@patch("module.EmailClient")
def test_send(mock_client_cls, mock_email_now):
mock_email_now.side_effect = ["2026-01-01T00:00:00Z"]
# ... setup ...
expected_email = Email(
subject="Hello",
send_at="2026-01-01T00:00:00Z", # Exact value from mocked source
)
assert mock_client.mock_calls == [call.send(expected_email)] # Exact match!
NEVER verify at method or attribute level:
mock_response.read.mock_calls (FORBIDDEN - method level)mock_response.read.return_value.decode.mock_calls (FORBIDDEN - nested attribute level)NEVER check length or index mock_calls:
assert len(mock.mock_calls) == 3 (FORBIDDEN - checking length)assert mock.mock_calls[0] == call(...) (FORBIDDEN - indexing)assert call(...) == mock.mock_calls[1] (FORBIDDEN - indexing with reversed order)NEVER use variables in expected values:
call(f"URL: {tested_url}") (FORBIDDEN - using variable)call(tested_value) (FORBIDDEN - using variable)call("URL: http://example.com") (CORRECT)ALWAYS use:
mock.mock_calls property at the object level for verificationassert mock_object.mock_calls == exp_calls (single assertion)For comprehensive mock patterns and examples, see references/mock-patterns.md.
When testing multiple scenarios for the same method, use these approaches in order of preference:
Use @pytest.mark.parametrize with pytest.param to define each scenario:
@pytest.mark.parametrize("input_value,expected", [
pytest.param(5, 10, id="positive"),
pytest.param(-5, 0, id="negative"),
pytest.param(0, 5, id="zero"),
])
def test_add_five(input_value, expected):
tested = Calculator()
result = tested.add_five(input_value)
assert result == expected
Benefits:
id parameterFor simple scenarios with similar setup:
def test_validate_email():
tested = Validator()
test_cases = [
("[email protected]", True),
("invalid.email", False),
("@example.com", False),
]
for email, expected in test_cases:
result = tested.validate_email(email)
assert result == expected
For complex scenarios requiring different setup or mocks:
def test_process_data__valid_input():
# Complex setup for valid input
pass
def test_process_data__invalid_format():
# Different setup for invalid format
pass
def test_process_data__network_error():
# Mock network error scenario
pass
For detailed parametrization patterns, see references/parametrize-examples.md.
Use mocks for:
datetime.now(), random.random(), uuid.uuid4()Example with internal method dependency:
class DataProcessor:
def validate(self, data):
# Complex validation logic tested separately
...
def process(self, data):
if not self.validate(data):
raise ValueError("Invalid data")
# Processing logic
return transformed_data
# GOOD - Mock validate() to focus on process() logic only
@patch.object(DataProcessor, "validate")
def test_process(mock_validate):
mock_validate.side_effect = [True]
tested = DataProcessor()
result = tested.process({"key": "value"})
expected = {"key": "transformed"}
assert result == expected
exp_validate_calls = [call({"key": "value"})]
assert mock_validate.mock_calls == exp_validate_calls
# BAD - Testing validate() behavior again inside process() tests
def test_process__duplicates_validation_tests():
tested = DataProcessor()
# This test ends up re-testing validate() logic
result = tested.process({"key": "value"})
...
Example with datetime:
@patch('module.datetime')
def test_create_timestamp(mock_datetime):
mock_datetime.now.side_effect = [datetime(2024, 1, 1, 12, 0, 0)]
tested = TimestampGenerator()
result = tested.create_timestamp()
expected = "2024-01-01 12:00:00"
assert result == expected
exp_calls = [call.now()]
assert mock_datetime.mock_calls == exp_calls
Prefer real instances over mocks for simple data objects. Mocks should isolate code from external dependencies, not replace simple data structures.
Use real instances when:
IMPORTANT for NamedTuples and Dataclasses:
Example - Prefer real instance:
from hook_information import HookInformation
# GOOD - HookInformation is a simple NamedTuple, use real instance
def test_session_directory():
hook_info = HookInformation(
session_id="test123",
exit_reason="user_exit",
transcript_path=Path("/path/to/transcript.jsonl"),
workspace_dir=Path("/home/user/project"),
working_directory=Path("/home/user/project/subdir"),
)
result = UserInputsLogger.session_directory(hook_info)
expected = Path("/home/user/project/.artifacts/user_inputs")
assert result == expected
Example - Avoid unnecessary mocks:
# BAD - Using Mock for a simple data object adds indirection without benefit
def test_session_directory():
mock_hook_info = Mock()
mock_hook_info.workspace_dir = Path("/home/user/project")
result = UserInputsLogger.session_directory(mock_hook_info)
# ...
Why prefer real instances:
Rule of thumb: If the object is a data container with no behavior to mock (no methods that perform I/O, no side effects), use a real instance.
Tests must pass these commands without errors:
uv run pytest tests/
uv run pytest -v tests/ --cov=.
Coverage must reach 100% for all source files.
uv run pytest tests/utils/test_parser.py -v
See examples/complete-test-file.py for a full working example that demonstrates:
side_effect and mock_callsSee examples/namedtuple-test-file.py for a full working example that demonstrates:
is_namedtuple() helperSee examples/dataclass-test-file.py for a full working example that demonstrates:
is_dataclass() helperFor detailed guidance on specific topics:
references/naming-conventions.md - Comprehensive naming examples for all scenariosreferences/mock-patterns.md - Mock patterns with side_effect and mock_calls verificationreferences/parametrize-examples.md - Advanced parametrization techniquesWorking examples demonstrating guidelines:
examples/conftest.py - Example conftest.py with is_namedtuple() and is_dataclass() helper functionsexamples/complete-test-file.py - Complete test file with source code showing all patterns for regular classesexamples/namedtuple-test-file.py - Complete test file demonstrating NamedTuple testing patternsexamples/dataclass-test-file.py - Complete test file demonstrating dataclass testing patternsTest structure:
test_inheritance() verifying issubclass(DerivedClass, BaseClass)test_class() using is_namedtuple(tested, fields) helper (add helper to tests/conftest.py)test_class() using is_dataclass(tested, fields) helper with string type values (add helper to
tests/conftest.py)Test naming: test_method_name or test_method_name__case
Variable naming (MANDATORY - use these exact names):
tested - the class or instance being tested (assign FIRST, call methods on it)result - the return value from the method under test (NEVER use output, ret, actual)expected - the expected value for single assertions (NEVER inline in assert)exp_* - expected values for multiple assertions (NEVER use expected_* prefix)Assertions:
is for singletons: assert result is True, assert result is None== for everything else: assert result == "value"Mock setup: Always use side_effect - mock.method.side_effect = [return_value]
Mock return objects: Use SimpleNamespace NOT MagicMock for return objects (like HTTP responses). Use lambdas for methods:
SimpleNamespace(status_code=200, json=lambda: {"data": "value"})
Capturing print output: Use pytest's capsys fixture - NEVER mock print. Pattern: captured = capsys.readouterr() then
assert "message" in captured.out
Mock scope: Mock external systems, I/O, non-deterministic behavior, and internal class method dependencies. Use real instances for simple data objects (NamedTuples, dataclasses). For dataclasses, you may mock internal methods when testing other methods to avoid duplication.
Mock verification: Verify ALL mock objects at object level with single assertion and hard-coded values
assert mock_obj.mock_calls == exp_calls (single assertion)assert mock_obj.method.mock_calls == exp_callsassert len(mock.mock_calls) == 3assert mock.mock_calls[0] == call(...)call(f"URL: {url_var}")call("URL: http://example.com")Parametrize: @pytest.mark.parametrize("arg,expected", [pytest.param(..., id="case")])
Coverage: uv run pytest tests/ --cov=. -v must show 100%
Excluded from tests: if __name__ == "__main__": blocks - do NOT write tests for these (excluded from coverage)
Critical Rules:
test_inheritance() as the first test when testing a class that inherits from a base class, OR test_class() using
is_namedtuple() helper for NamedTuple classes, OR test_class() using is_dataclass() helper for dataclass classestested variable FIRST, then call methods on tested - NEVER call methods directly on the class nameresult - NEVER use output, ret, actual, response, etc.expected - NEVER inline expected values in assertionsexp_* prefix for multiple expected values (e.g., exp_calls) - NEVER use expected_* prefixreturn_value to set what a mock returns - always use side_effectmock_calls in EVERY test - missing verification is forbiddenmock_calls even in parametrized testsmock.mock_calls), NEVER at method level (mock.method.mock_calls)assert mock.mock_calls == exp_callsassert len(mock.mock_calls) == 3 or mock.mock_calls[0]assert_called_with() - only use mock_callsis for True/False/None comparisons - never use == for singletonsSimpleNamespace NOT MagicMock for return objects (HTTP responses, etc.) - avoids forgetting mock_calls verificationcapsys fixture to capture print output - NEVER mock printif __name__ == "__main__": blocks - they are excluded from coverageApply these guidelines consistently to create maintainable, comprehensive test suites that ensure code reliability and prevent regressions.
npx claudepluginhub canvas-medical/coding-agents --plugin pytest-forgeGuides Python testing with pytest: TDD cycle, fixture patterns, mocking, parametrization, and 80%+ coverage targets. Activates when writing Python tests or setting up coverage infrastructure.
pytest testing framework conventions and practices. Invoke whenever task involves any interaction with pytest — writing tests, configuring pytest, fixtures, parametrize, mocking, debugging test failures, or coverage.
Guides Python testing with pytest, TDD, fixtures, mocking, parametrization, and coverage. Useful when writing Python code or reviewing test suites.