Python Testing Best Practices

SkillDev tools

Python testing best practices covering pytest, unittest.mock, property-based testing with Hypothesis, fixtures, and mocking patterns

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Python Testing Best Practices skill

What this skill tells your AI

The instructions your AI receives, as published by taurgis/has-marstek-local-api in .agents/skills/python-testing/SKILL.md and read by ahel’s review.

Expert-level guidance for writing effective, maintainable Python tests.

The Zen of Testing

Tests should be fast and deterministic.
Each test should verify one thing.
Tests are documentation—make them readable.
Mock at boundaries, not implementation details.
Prefer integration over unit when boundaries are unclear.
Failing tests should tell you what broke.
Test behavior, not implementation.

Test Organization

Directory Structure

tests/
├── conftest.py          # Shared fixtures, plugins
├── test_module.py       # Tests for module.py
├── test_feature/        # Feature-specific tests
│   ├── conftest.py      # Feature-specific fixtures
│   └── test_component.py
└── fixtures/            # Test data files
    └── sample_data.json

Naming Conventions

ElementConventionExample
Test filestest_*.py or *_test.pytest_calculator.py
Test classesTest* (optional)TestCalculator
Test functionstest_*test_add_positive_numbers
FixturesDescriptive nounsmock_database, sample_user

Use descriptive names: test_<what>_<condition>_<expected>

def test_divide_by_zero_raises_exception() -> None: ...
def test_login_with_invalid_password_returns_401() -> None: ...

pytest Quick Reference

AAA Pattern

def test_user_creation() -> None:
    # Arrange
    email = "test@example.com"

    # Act
    user = User.create(email=email)

    # Assert
    assert user.email == email

Key Assertions

assert value == expected
assert value is None / is not None
assert value in collection
assert value == pytest.approx(expected, rel=1e-3)  # floats

with pytest.raises(ValueError) as exc_info:
    function_that_raises()

Parametrized Tests

@pytest.mark.parametrize("input_val,expected", [
    (1, 2), (2, 4), (-1, -2),
])
def test_double(input_val: int, expected: int) -> None:
    assert double(input_val) == expected

Common Markers

@pytest.mark.slow            # Custom marker
@pytest.mark.skip(reason="") # Always skip
@pytest.mark.skipif(cond)    # Conditional skip
@pytest.mark.xfail           # Expected failure
@pytest.mark.asyncio         # Async test

See: references/PYTEST.md for fixtures, scopes, factories, teardown patterns.

unittest.mock Essentials

Core Concepts

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

mock = Mock(return_value=42)
mock = Mock(side_effect=[1, 2, 3])  # Sequential returns
mock = Mock(side_effect=ValueError)  # Raise exception

Where to Patch (Critical!)

Patch where the object is looked up, not where it's defined:

# mymodule.py
from datetime import datetime
def get_time(): return datetime.now()

# test_mymodule.py
@patch("mymodule.datetime")  # ✓ Correct: where used
def test_time(mock_dt): ...

@patch("datetime.datetime")  # ✗ Wrong: where defined
def test_wrong(mock_dt): ...

Key Assertions

mock.assert_called()
mock.assert_called_once()
mock.assert_called_with(*args, **kwargs)
mock.assert_called_once_with(*args, **kwargs)
mock.assert_not_called()
mock.assert_has_calls([call(...), call(...)])

Patch Variants

@patch("module.Class")              # Replace object
@patch.object(Class, "method")      # Patch attribute
@patch.dict(os.environ, {"K": "V"}) # Patch dict
@patch("module.Class", autospec=True)  # Enforce signature

See: references/MOCKING.md for AsyncMock, mock_open, ANY matcher, autospec details.

Hypothesis (Property-Based Testing)

Generate test cases to find edge cases automatically:

from hypothesis import given, strategies as st

@given(st.integers())
def test_abs_non_negative(n: int) -> None:
    assert abs(n) >= 0

@given(st.lists(st.integers()))
def test_sort_length(lst: list[int]) -> None:
    assert len(sorted(lst)) == len(lst)

See: references/HYPOTHESIS.md for strategies, assume(), composite strategies.

Async Testing

@pytest.mark.asyncio
async def test_async_function() -> None:
    result = await async_fetch()
    assert result == expected

# AsyncMock for mocking
mock_client = AsyncMock()
mock_client.get.return_value = {"data": "value"}
mock_client.get.assert_awaited_once_with("/path")

See: references/ASYNC_AND_COVERAGE.md for fixtures, timeouts, coverage config.

Coverage

pytest --cov=mypackage --cov-fail-under=95 tests/
pytest --cov=mypackage --cov-report=html tests/

Quick Checklist

Before committing tests:

  • AAA pattern (Arrange-Act-Assert)
  • Descriptive test names
  • One assertion concept per test
  • Mocks patch at lookup location
  • No shared mutable state
  • Fixtures handle teardown
  • Async tests use @pytest.mark.asyncio
  • Coverage meets threshold
  • Deterministic (no flaky tests)
  • autospec=True for signature safety

See: references/ANTI_PATTERNS.md for common mistakes to avoid.

Reference Files

TopicFile
pytest detailsreferences/PYTEST.md
Mocking patternsreferences/MOCKING.md
Property-based testingreferences/HYPOTHESIS.md
Async & coveragereferences/ASYNC_AND_COVERAGE.md
Anti-patternsreferences/ANTI_PATTERNS.md
Full examplesreferences/EXAMPLES.md

Signals

GitHub stars
35
Forks
5
Last commit
Sep 2026
Advanced
Item type
skill
Key
python-testing-taurgis
Source
github.com/taurgis/has-marstek-local-api