python-testing-standards

Python Testing Standards

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "python-testing-standards" with this command: npx skills add clostaunau/holiday-card/clostaunau-holiday-card-python-testing-standards

Python Testing Standards

Purpose

This skill provides comprehensive testing standards and best practices for Python projects using pytest. It serves as a reference guide during code reviews to ensure test quality, maintainability, and adherence to Python testing conventions.

When to use this skill:

  • Conducting code reviews of Python test files

  • Writing new test suites

  • Refactoring existing tests

  • Evaluating test coverage and quality

  • Teaching testing best practices to team members

Context

High-quality tests are essential for maintaining reliable Python applications. This skill documents industry-standard testing practices using pytest, the de facto testing framework for modern Python development. These standards emphasize:

  • Clarity: Tests should be easy to read and understand

  • Maintainability: Tests should be easy to update as code evolves

  • Reliability: Tests should be deterministic and fast

  • Coverage: Tests should cover behavior, not just lines of code

  • Isolation: Tests should be independent and not affect each other

This skill is designed to be referenced by the uncle-duke-python agent during code reviews and by developers when writing tests.

Prerequisites

Required Knowledge:

  • Python fundamentals

  • Basic understanding of testing concepts

  • Familiarity with pytest (or willingness to learn)

Required Tools:

  • pytest (pip install pytest )

  • pytest-cov for coverage (pip install pytest-cov )

  • pytest-mock for mocking (pip install pytest-mock )

  • unittest.mock (built into Python 3.3+)

Expected Project Structure:

project/ ├── src/ │ └── mypackage/ │ ├── init.py │ └── module.py ├── tests/ │ ├── init.py │ ├── conftest.py │ ├── unit/ │ │ └── test_module.py │ └── integration/ │ └── test_integration.py ├── pytest.ini └── requirements-dev.txt

Instructions

Task 1: Verify pytest Conventions

1.1 Test File Naming

Rule: Test files MUST follow one of these patterns:

  • test_*.py (preferred)

  • *_test.py

Examples:

✅ Good:

File: test_user_service.py

File: test_authentication.py

File: user_service_test.py # acceptable but less common

❌ Bad:

File: user_tests.py # missing 'test_' prefix

File: test-user-service.py # hyphens instead of underscores

File: TestUserService.py # CamelCase instead of snake_case

Why: pytest discovers test files by these patterns. Non-standard names won't be automatically discovered.

1.2 Test Function Naming

Rule: Test functions MUST start with test_ and describe the scenario being tested.

Pattern: test_<what><when><expected_behavior>

✅ Good:

def test_user_login_with_valid_credentials_returns_token(): """Test that valid credentials return an auth token.""" pass

def test_user_login_with_invalid_password_raises_authentication_error(): """Test that invalid password raises AuthenticationError.""" pass

def test_calculate_total_with_empty_cart_returns_zero(): """Test that empty cart returns zero total.""" pass

❌ Bad:

def test_login(): # Too vague pass

def test_user_service(): # Doesn't describe what's being tested pass

def testLogin(): # Missing underscore, CamelCase pass

def test_1(): # Non-descriptive pass

Why: Descriptive test names serve as documentation. When a test fails, the name should immediately tell you what broke.

1.3 Test Class Naming

Rule: Test classes MUST start with Test (no _test suffix).

✅ Good:

class TestUserAuthentication: """Tests for user authentication functionality."""

def test_login_with_valid_credentials_succeeds(self):
    pass

def test_login_with_invalid_credentials_fails(self):
    pass

class TestShoppingCart: """Tests for shopping cart operations."""

def test_add_item_increases_count(self):
    pass

❌ Bad:

class UserAuthenticationTests: # Missing 'Test' prefix pass

class Test_UserAuth: # Unnecessary underscore pass

class testUserAuth: # Lowercase 'test' pass

Why: pytest discovers test classes by the Test prefix. Classes provide logical grouping of related tests.

When to use classes vs functions:

  • Use classes when tests share setup/teardown logic or fixtures

  • Use functions for simple, independent tests

  • Don't use classes just for grouping without shared behavior

1.4 Test Organization and Structure

Directory Structure:

tests/ ├── init.py # Makes tests a package ├── conftest.py # Shared fixtures and configuration ├── unit/ # Unit tests (fast, isolated) │ ├── init.py │ ├── test_models.py │ ├── test_services.py │ └── test_utils.py ├── integration/ # Integration tests (slower, external deps) │ ├── init.py │ ├── test_database.py │ └── test_api_endpoints.py └── e2e/ # End-to-end tests (slowest) ├── init.py └── test_user_workflows.py

File Organization:

Within a test file, organize tests logically:

"""Tests for user authentication service."""

import pytest from myapp.auth import AuthenticationService

Fixtures at the top

@pytest.fixture def auth_service(): """Fixture providing authentication service instance.""" return AuthenticationService()

Test classes grouped by functionality

class TestUserLogin: """Tests for user login functionality."""

def test_login_with_valid_credentials_succeeds(self, auth_service):
    pass

def test_login_with_invalid_password_fails(self, auth_service):
    pass

class TestUserLogout: """Tests for user logout functionality."""

def test_logout_invalidates_session(self, auth_service):
    pass

Standalone functions for simple tests

def test_password_hashing_is_deterministic(): """Test that same password always produces same hash.""" pass

Task 2: Apply Test Structure Patterns

2.1 Arrange-Act-Assert (AAA) Pattern

Rule: Structure all tests using the AAA pattern with clear separation.

Pattern:

  • Arrange: Set up test data and preconditions

  • Act: Execute the behavior being tested

  • Assert: Verify the expected outcome

✅ Good:

def test_calculate_total_with_discount_applies_correctly(): """Test that discount is correctly applied to cart total.""" # Arrange cart = ShoppingCart() cart.add_item(Item(name="Widget", price=100.00)) cart.add_item(Item(name="Gadget", price=50.00)) discount = Discount(percentage=10)

# Act
total = cart.calculate_total(discount=discount)

# Assert
assert total == 135.00  # (100 + 50) * 0.9

✅ Good (with comments):

def test_user_registration_creates_new_user(): """Test that user registration creates a new user record.""" # Arrange user_data = { "username": "testuser", "email": "test@example.com", "password": "SecurePass123!" } user_service = UserService()

# Act
user = user_service.register(user_data)

# Assert
assert user.username == "testuser"
assert user.email == "test@example.com"
assert user.id is not None
assert user.password_hash != user_data["password"]  # Hashed

❌ Bad:

def test_user_operations(): # Everything mixed together user_service = UserService() user = user_service.register({"username": "test", "email": "test@example.com"}) assert user.username == "test" user.update_email("new@example.com") assert user.email == "new@example.com" user_service.delete(user.id) assert user_service.get(user.id) is None

Why bad: Tests multiple behaviors, hard to debug when it fails, violates single responsibility.

2.2 Given-When-Then (BDD Pattern)

Alternative to AAA: Given-When-Then is equivalent but uses BDD terminology.

Pattern:

  • Given: Preconditions and setup (same as Arrange)

  • When: Action being tested (same as Act)

  • Then: Expected outcome (same as Assert)

✅ Good:

def test_withdraw_with_sufficient_balance_succeeds(): """ Given an account with a balance of $100 When withdrawing $30 Then the withdrawal succeeds and balance is $70 """ # Given account = Account(balance=100.00)

# When
result = account.withdraw(30.00)

# Then
assert result is True
assert account.balance == 70.00

Usage: Use Given-When-Then for behavior-driven tests, especially in integration/e2e tests. Use AAA for unit tests. Be consistent within a project.

2.3 Test Isolation

Rule: Each test MUST be completely independent and not rely on execution order.

✅ Good:

@pytest.fixture def clean_database(): """Provide a clean database for each test.""" db = Database() db.clear() yield db db.clear() # Cleanup after test

def test_create_user_adds_to_database(clean_database): """Test user creation adds record to database.""" user = User(username="test") clean_database.save(user)

assert clean_database.count() == 1

def test_delete_user_removes_from_database(clean_database): """Test user deletion removes record from database.""" user = User(username="test") clean_database.save(user) clean_database.delete(user.id)

assert clean_database.count() == 0

❌ Bad:

Tests depend on execution order

db = Database()

def test_1_create_user(): """This test must run first.""" user = User(username="test") db.save(user) assert db.count() == 1

def test_2_delete_user(): """This test depends on test_1 running first.""" # Assumes user from test_1 exists users = db.get_all() db.delete(users[0].id) assert db.count() == 0

Why bad: Tests are brittle, fail when run in isolation or different order, hard to debug.

2.4 Test Data Management

Rule: Use fixtures, factories, or builders for test data. Avoid magic values.

✅ Good:

@pytest.fixture def sample_user(): """Provide a sample user for testing.""" return User( username="john_doe", email="john@example.com", age=30, is_active=True )

@pytest.fixture def user_factory(): """Provide a factory for creating test users.""" def _create_user(**kwargs): defaults = { "username": "test_user", "email": "test@example.com", "age": 25, "is_active": True } defaults.update(kwargs) return User(**defaults) return _create_user

def test_user_validation_with_invalid_age(user_factory): """Test that invalid age raises validation error.""" user = user_factory(age=-5)

with pytest.raises(ValidationError):
    user.validate()

❌ Bad:

def test_user_creation(): # Magic values scattered throughout user = User("john", "john@example.com", 30, True) assert user.username == "john"

def test_user_update(): # Same magic values repeated user = User("john", "john@example.com", 30, True) user.update_age(31) assert user.age == 31

Why bad: Hard to maintain, unclear what values mean, violates DRY principle.

Task 3: Implement Fixture Best Practices

3.1 Fixture Scopes

Rule: Choose the appropriate scope based on fixture cost and state.

Available Scopes:

  • function : Default, runs before each test function (most common)

  • class : Runs once per test class

  • module : Runs once per module

  • session : Runs once per test session

✅ Good:

@pytest.fixture(scope="session") def database_engine(): """Create database engine once per test session.""" engine = create_engine("postgresql://localhost/test_db") yield engine engine.dispose()

@pytest.fixture(scope="module") def database_schema(database_engine): """Create database schema once per module.""" Base.metadata.create_all(database_engine) yield Base.metadata.drop_all(database_engine)

@pytest.fixture(scope="function") def database_session(database_engine): """Provide a clean database session for each test.""" connection = database_engine.connect() transaction = connection.begin() session = Session(bind=connection)

yield session

session.close()
transaction.rollback()
connection.close()

Scope Selection Guidelines:

  • Use function scope for fixtures that need to be fresh for each test

  • Use module or session scope for expensive setup (database connections, API clients)

  • NEVER use broader scope for fixtures that maintain state between tests

  • Always clean up in broader-scoped fixtures

❌ Bad:

@pytest.fixture(scope="session") def user_list(): """DON'T DO THIS - mutable state in session scope.""" return [] # This list will be shared across ALL tests!

def test_add_user(user_list): user_list.append("user1") assert len(user_list) == 1 # Might fail if other tests run first

def test_remove_user(user_list): # Depends on state from other tests user_list.remove("user1")

Why bad: Shared mutable state causes tests to interfere with each other.

3.2 Fixture Dependencies

Rule: Fixtures can depend on other fixtures to build complex test scenarios.

✅ Good:

@pytest.fixture def database(): """Provide database connection.""" db = Database("test.db") yield db db.close()

@pytest.fixture def user(database): """Provide a test user in the database.""" user = User(username="testuser") database.save(user) return user

@pytest.fixture def authenticated_user(user): """Provide an authenticated user.""" user.login() return user

def test_user_can_access_profile(authenticated_user, database): """Test authenticated user can access their profile.""" profile = database.get_profile(authenticated_user.id) assert profile is not None assert profile.user_id == authenticated_user.id

Dependency Chain: database → user → authenticated_user

This approach builds complex test scenarios from simple, reusable fixtures.

3.3 Fixture Naming Conventions

Rule: Fixture names should be clear, descriptive nouns or noun phrases.

✅ Good:

@pytest.fixture def database_session(): """Provide a database session.""" pass

@pytest.fixture def mock_email_service(): """Provide a mocked email service.""" pass

@pytest.fixture def sample_user_data(): """Provide sample user data dictionary.""" pass

@pytest.fixture def http_client(): """Provide an HTTP client for API testing.""" pass

❌ Bad:

@pytest.fixture def get_db(): # Verb, not noun pass

@pytest.fixture def data(): # Too vague pass

@pytest.fixture def fixture1(): # Non-descriptive pass

@pytest.fixture def temp(): # Unclear what it provides pass

3.4 Fixtures vs Helper Functions

Rule: Use fixtures for setup/teardown and state. Use helper functions for operations.

✅ Good:

Fixture for state/setup

@pytest.fixture def user(): """Provide a test user.""" return User(username="test")

Helper function for operations

def create_post(user, title, content): """Helper to create a post for testing.""" return Post(author=user, title=title, content=content)

def test_user_can_create_post(user): """Test that user can create a post.""" post = create_post(user, "Test Title", "Test Content")

assert post.author == user
assert post.title == "Test Title"

When to use fixtures:

  • Setting up test data or objects

  • Managing resources (database connections, file handles)

  • Setup/teardown logic

  • Sharing state across multiple tests

When to use helper functions:

  • Performing operations in tests

  • Reducing code duplication in test bodies

  • Complex assertions

  • Test data generation with many parameters

3.5 autouse Fixtures

Rule: Use autouse=True sparingly, only for fixtures that should always run.

✅ Good:

@pytest.fixture(autouse=True) def reset_global_state(): """Reset global application state before each test.""" AppConfig.reset() Cache.clear() yield # Cleanup after test

@pytest.fixture(autouse=True, scope="session") def configure_logging(): """Configure logging for test session.""" logging.basicConfig(level=logging.DEBUG)

❌ Bad:

@pytest.fixture(autouse=True) def create_test_user(): """DON'T DO THIS - not all tests need a user.""" return User(username="test") # Wastes resources for tests that don't need it

Use autouse when:

  • Resetting global state

  • Configuring logging/warnings

  • Setting up test environment variables

  • Cleanup that should always happen

Don't use autouse when:

  • Only some tests need the fixture

  • The fixture is expensive to create

  • It makes test dependencies unclear

3.6 conftest.py Best Practices

Rule: Place shared fixtures in conftest.py at appropriate levels.

Directory Structure:

tests/ ├── conftest.py # Shared across ALL tests ├── unit/ │ ├── conftest.py # Shared across unit tests only │ └── test_services.py └── integration/ ├── conftest.py # Shared across integration tests only └── test_database.py

Example conftest.py:

"""Shared fixtures for all tests."""

import pytest from myapp import create_app from myapp.database import Database

@pytest.fixture(scope="session") def app(): """Provide application instance for testing.""" app = create_app(testing=True) return app

@pytest.fixture def client(app): """Provide test client for making HTTP requests.""" return app.test_client()

@pytest.fixture def database(): """Provide clean database for testing.""" db = Database(":memory:") db.create_schema() yield db db.close()

Register custom markers

def pytest_configure(config): """Register custom markers.""" config.addinivalue_line( "markers", "slow: marks tests as slow (deselect with '-m "not slow"')" ) config.addinivalue_line( "markers", "integration: marks tests as integration tests" )

Task 4: Apply Parametrization

Rule: Use @pytest.mark.parametrize to test multiple inputs without duplicating code.

✅ Good:

@pytest.mark.parametrize("value,expected", [ (0, False), (1, True), (42, True), (-1, True), (-42, True), ]) def test_is_non_zero(value, expected): """Test is_non_zero function with various inputs.""" assert is_non_zero(value) == expected

Multiple Parameters:

@pytest.mark.parametrize("username,email,valid", [ ("john_doe", "john@example.com", True), ("", "john@example.com", False), # Empty username ("john_doe", "", False), # Empty email ("john_doe", "invalid-email", False), # Invalid email format ("ab", "short@example.com", False), # Username too short ]) def test_user_validation(username, email, valid): """Test user validation with various inputs.""" user = User(username=username, email=email)

if valid:
    user.validate()  # Should not raise
else:
    with pytest.raises(ValidationError):
        user.validate()

Using pytest.param for Test IDs:

@pytest.mark.parametrize("input_data,expected", [ pytest.param( {"username": "john", "age": 30}, User(username="john", age=30), id="valid_user" ), pytest.param( {"username": "", "age": 30}, ValidationError, id="empty_username" ), pytest.param( {"username": "john", "age": -5}, ValidationError, id="negative_age" ), ]) def test_user_creation(input_data, expected): """Test user creation with various inputs.""" if isinstance(expected, type) and issubclass(expected, Exception): with pytest.raises(expected): User(**input_data) else: user = User(**input_data) assert user.username == expected.username assert user.age == expected.age

Parametrizing Fixtures:

@pytest.fixture(params=["sqlite", "postgresql", "mysql"]) def database(request): """Provide different database backends for testing.""" db_type = request.param

if db_type == "sqlite":
    db = SQLiteDatabase(":memory:")
elif db_type == "postgresql":
    db = PostgreSQLDatabase("localhost", "test_db")
elif db_type == "mysql":
    db = MySQLDatabase("localhost", "test_db")

db.create_schema()
yield db
db.drop_schema()
db.close()

def test_database_insert(database): """Test insert works on all database backends.""" # This test runs 3 times, once for each database type database.insert("users", {"username": "test"}) assert database.count("users") == 1

Task 5: Implement Mocking and Patching Best Practices

5.1 When to Mock vs When Not to Mock

Mock:

  • External services (APIs, databases, email services)

  • Slow operations (file I/O, network calls)

  • Non-deterministic operations (random, datetime)

  • Side effects you want to verify (logging, events)

Don't Mock:

  • Simple data structures (dictionaries, lists)

  • Pure functions with no side effects

  • Your own internal business logic (test it directly)

  • Value objects and entities

✅ Good (mocking external service):

def test_send_welcome_email_calls_email_service(mocker): """Test that user registration sends welcome email.""" # Mock external email service mock_email = mocker.patch("myapp.services.EmailService.send")

user_service = UserService()
user = user_service.register("john@example.com")

# Verify email service was called
mock_email.assert_called_once_with(
    to="john@example.com",
    subject="Welcome!",
    template="welcome"
)

❌ Bad (mocking internal logic):

def test_calculate_total(mocker): """DON'T DO THIS - mocking the thing you're testing.""" cart = ShoppingCart()

# Mocking internal method defeats the purpose of testing
mocker.patch.object(cart, "calculate_total", return_value=100.0)

assert cart.calculate_total() == 100.0  # Pointless test

5.2 unittest.mock Usage

Basic Mocking:

from unittest.mock import Mock, MagicMock, patch

def test_user_service_with_mock_database(): """Test user service with mocked database.""" # Create a mock database mock_db = Mock() mock_db.save.return_value = True mock_db.get.return_value = User(id=1, username="test")

user_service = UserService(database=mock_db)
user = user_service.create_user("test")

# Verify interactions
mock_db.save.assert_called_once()
assert user.username == "test"

Using MagicMock for Magic Methods:

def test_context_manager(): """Test code that uses context managers.""" mock_file = MagicMock() mock_file.enter.return_value = mock_file mock_file.read.return_value = "test content"

with mock_file as f:
    content = f.read()

assert content == "test content"
mock_file.__enter__.assert_called_once()
mock_file.__exit__.assert_called_once()

5.3 pytest-mock Plugin

Preferred Approach: Use pytest-mock's mocker fixture for cleaner syntax.

✅ Good:

def test_get_user_data_from_api(mocker): """Test fetching user data from external API.""" # Mock the requests.get call mock_get = mocker.patch("requests.get") mock_get.return_value.json.return_value = { "id": 1, "name": "John Doe" } mock_get.return_value.status_code = 200

api_client = APIClient()
user_data = api_client.get_user(user_id=1)

# Verify API was called correctly
mock_get.assert_called_once_with(
    "https://api.example.com/users/1",
    headers={"Authorization": "Bearer token"}
)
assert user_data["name"] == "John Doe"

Mocking Class Methods:

def test_service_calls_repository(mocker): """Test that service layer calls repository correctly.""" # Mock the repository method mock_find = mocker.patch("myapp.repositories.UserRepository.find_by_email") mock_find.return_value = User(id=1, email="test@example.com")

service = UserService()
user = service.get_user_by_email("test@example.com")

mock_find.assert_called_once_with("test@example.com")
assert user.email == "test@example.com"

5.4 Patching Best Practices

Rule: Patch where the object is used, not where it's defined.

✅ Good:

myapp/services.py

from myapp.repositories import UserRepository

class UserService: def get_user(self, user_id): return UserRepository.find(user_id)

tests/test_services.py

def test_get_user(mocker): """Patch where UserRepository is USED.""" # Correct: patch in myapp.services where it's imported mock_find = mocker.patch("myapp.services.UserRepository.find") mock_find.return_value = User(id=1)

service = UserService()
user = service.get_user(1)

assert user.id == 1

❌ Bad:

def test_get_user(mocker): """DON'T DO THIS - patching where it's defined.""" # Wrong: patch in myapp.repositories where it's defined mock_find = mocker.patch("myapp.repositories.UserRepository.find") # This won't work because UserService imported it before the patch

Patching Built-ins:

def test_file_processing(mocker): """Test file processing with mocked open.""" mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="test data"))

processor = FileProcessor()
content = processor.read_file("test.txt")

mock_open.assert_called_once_with("test.txt", "r")
assert content == "test data"

Patching datetime:

from datetime import datetime

def test_timestamp_generation(mocker): """Test timestamp generation with frozen time.""" # Mock datetime.now() mock_datetime = mocker.patch("myapp.utils.datetime") mock_datetime.now.return_value = datetime(2024, 1, 1, 12, 0, 0)

timestamp = generate_timestamp()

assert timestamp == "2024-01-01 12:00:00"

5.5 Mock Assertions

Rule: Always verify that mocks were called correctly.

Common Assertions:

def test_mock_assertions(mocker): """Demonstrate common mock assertions.""" mock_service = mocker.Mock()

# Call the mock
mock_service.send_email("test@example.com", "Hello")
mock_service.send_email("other@example.com", "World")

# Verify it was called
assert mock_service.send_email.called
assert mock_service.send_email.call_count == 2

# Verify specific calls
mock_service.send_email.assert_called_with("other@example.com", "World")
mock_service.send_email.assert_any_call("test@example.com", "Hello")

# Verify all calls
assert mock_service.send_email.call_args_list == [
    mocker.call("test@example.com", "Hello"),
    mocker.call("other@example.com", "World"),
]

Verify Mock Not Called:

def test_service_does_not_send_email_for_inactive_users(mocker): """Test that inactive users don't receive emails.""" mock_email = mocker.patch("myapp.services.EmailService.send")

user_service = UserService()
user_service.notify_user(User(is_active=False))

# Verify email was NOT sent
mock_email.assert_not_called()

Side Effects:

def test_retry_logic_with_failures(mocker): """Test retry logic when API calls fail.""" mock_api = mocker.Mock() # First two calls raise exception, third succeeds mock_api.fetch.side_effect = [ ConnectionError("Failed"), ConnectionError("Failed"), {"status": "success"} ]

client = APIClient(api=mock_api)
result = client.fetch_with_retry(max_retries=3)

assert result == {"status": "success"}
assert mock_api.fetch.call_count == 3

Task 6: Implement Code Coverage Best Practices

6.1 Coverage Thresholds

Recommended Thresholds:

  • Overall project: 80% minimum

  • New code: 90% minimum (enforce in CI)

  • Critical paths: 100% (authentication, payment, security)

pytest.ini Configuration:

[pytest]

Run with coverage by default in CI

addopts = --cov=myapp --cov-report=html --cov-report=term --cov-fail-under=80

Coverage options

[coverage:run] omit = /tests/ /migrations/ /venv/ /pycache/ /site-packages/

[coverage:report] exclude_lines = pragma: no cover def repr raise AssertionError raise NotImplementedError if name == .main.: if TYPE_CHECKING: @abstractmethod

Running Coverage:

Generate coverage report

pytest --cov=myapp --cov-report=html --cov-report=term

View HTML report

open htmlcov/index.html

Fail if coverage below threshold

pytest --cov=myapp --cov-fail-under=80

Show missing lines

pytest --cov=myapp --cov-report=term-missing

6.2 What to Test (and What Not to Test)

DO Test:

✅ Business Logic:

def calculate_discount(price, customer_tier): """Calculate discount based on customer tier.""" if customer_tier == "gold": return price * 0.20 elif customer_tier == "silver": return price * 0.10 return 0.0

TEST THIS - it's core business logic

def test_calculate_discount_for_gold_tier(): assert calculate_discount(100.0, "gold") == 20.0

✅ Edge Cases and Boundaries:

def test_get_user_with_nonexistent_id_raises_error(): """Test that fetching non-existent user raises NotFound.""" with pytest.raises(NotFoundError): user_service.get_user(user_id=999999)

def test_divide_by_zero_raises_error(): """Test division by zero raises appropriate error.""" with pytest.raises(ZeroDivisionError): calculator.divide(10, 0)

✅ Error Handling:

def test_invalid_email_format_raises_validation_error(): """Test that invalid email format raises ValidationError.""" with pytest.raises(ValidationError, match="Invalid email format"): User(email="not-an-email")

✅ Public API/Interface:

class UserService: def register(self, email, password): # Public API - TEST """Register a new user.""" user = self._create_user(email) # Private - don't test directly self._send_welcome_email(user) # Private - don't test directly return user

Test the public method, not private helpers

def test_register_creates_user_and_sends_email(mocker): mock_email = mocker.patch("myapp.services.EmailService.send")

user = user_service.register("test@example.com", "password")

assert user.email == "test@example.com"
mock_email.assert_called_once()

DON'T Test:

❌ Framework/Library Code:

DON'T test that Django's ORM works

def test_user_save(): """DON'T DO THIS - testing Django, not your code.""" user = User(username="test") user.save() assert User.objects.filter(username="test").exists()

❌ Trivial Getters/Setters:

class User: @property def username(self): return self._username # DON'T test this

@username.setter
def username(self, value):
    self._username = value  # DON'T test this

❌ Private Implementation Details:

class Calculator: def add(self, a, b): return self._perform_addition(a, b)

def _perform_addition(self, a, b):  # Private method
    return a + b

DON'T test _perform_addition directly

Test the public add() method instead

❌ Generated Code:

DON'T test auto-generated migration files, init.py, etc.

6.3 Coverage Tools

pytest-cov:

Install

pip install pytest-cov

Basic usage

pytest --cov=myapp

With HTML report

pytest --cov=myapp --cov-report=html

Show missing lines

pytest --cov=myapp --cov-report=term-missing

Multiple formats

pytest --cov=myapp --cov-report=html --cov-report=term --cov-report=xml

Coverage.py (underlying tool):

Run tests with coverage

coverage run -m pytest

Generate report

coverage report

HTML report

coverage html

Combine coverage from multiple runs

coverage combine coverage report

Branch Coverage:

[coverage:run] branch = True # Enable branch coverage (not just line coverage)

def example(x): if x > 0: # Branch 1 return "positive" else: # Branch 2 return "non-positive"

Line coverage: 100% if you test with x=1

Branch coverage: 50% - you need to test both x>0 and x<=0

Task 7: Avoid Common Anti-Patterns

7.1 Anti-Pattern: Testing Implementation Details

❌ Bad:

def test_user_service_implementation_details(): """DON'T DO THIS - testing how it works, not what it does.""" service = UserService()

# Testing that internal _validate method is called
with patch.object(service, "_validate") as mock_validate:
    service.register("test@example.com")
    mock_validate.assert_called_once()

✅ Good:

def test_user_service_validates_email(): """Test the behavior: invalid emails are rejected.""" service = UserService()

# Test the behavior, not the implementation
with pytest.raises(ValidationError):
    service.register("invalid-email")

Why: Implementation details change. Tests should verify behavior, not how the behavior is achieved.

7.2 Anti-Pattern: Overly Complex Test Setup

❌ Bad:

def test_complex_scenario(): """DON'T DO THIS - setup is too complex.""" # 50 lines of setup code db = Database() db.connect() db.create_tables() user1 = User(username="user1") user2 = User(username="user2") db.save(user1) db.save(user2) post1 = Post(author=user1, title="Post 1") post2 = Post(author=user2, title="Post 2") db.save(post1) db.save(post2) comment1 = Comment(post=post1, author=user2, text="Comment") db.save(comment1) # ... many more lines

# Finally, the actual test
result = service.get_posts()
assert len(result) == 2

✅ Good:

@pytest.fixture def database_with_posts(): """Provide database with test posts.""" db = Database() db.create_schema()

users = [User(username=f"user{i}") for i in range(2)]
posts = [Post(author=users[i], title=f"Post {i}") for i in range(2)]

for user in users:
    db.save(user)
for post in posts:
    db.save(post)

yield db
db.close()

def test_get_posts(database_with_posts): """Test retrieving posts.""" result = service.get_posts() assert len(result) == 2

Why: Complex setup makes tests hard to read and maintain. Use fixtures and factories.

7.3 Anti-Pattern: Flaky Tests

❌ Bad:

import time import random

def test_flaky_timing(): """DON'T DO THIS - test depends on timing.""" start = time.time() process_data() duration = time.time() - start

# Flaky: might fail on slow systems
assert duration &#x3C; 1.0

def test_flaky_random(): """DON'T DO THIS - test uses uncontrolled randomness.""" result = generate_random_value() assert result > 5 # Might fail randomly

def test_flaky_order(): """DON'T DO THIS - test depends on iteration order.""" users = User.objects.all() # Order not guaranteed assert users[0].username == "alice"

✅ Good:

def test_with_mocked_time(mocker): """Test with controlled time.""" mock_time = mocker.patch("time.time") mock_time.side_effect = [0.0, 0.5] # Controlled values

start = time.time()
process_data()
duration = time.time() - start

assert duration == 0.5

def test_with_seeded_random(mocker): """Test with controlled randomness.""" mocker.patch("random.randint", return_value=7)

result = generate_random_value()
assert result == 7

def test_with_explicit_order(): """Test with guaranteed order.""" users = User.objects.all().order_by("username") assert users[0].username == "alice"

Why: Flaky tests erode trust in the test suite and waste developer time.

7.4 Anti-Pattern: Too Many Assertions in One Test

❌ Bad:

def test_user_operations(): """DON'T DO THIS - testing multiple behaviors.""" # Test 1: User creation user = User(username="test", email="test@example.com") assert user.username == "test" assert user.email == "test@example.com"

# Test 2: User validation
user.validate()
assert user.is_valid is True

# Test 3: User saving
user.save()
assert user.id is not None

# Test 4: User updating
user.username = "updated"
user.save()
assert user.username == "updated"

# Test 5: User deletion
user.delete()
assert User.objects.filter(id=user.id).count() == 0

✅ Good:

def test_user_creation(): """Test user is created with correct attributes.""" user = User(username="test", email="test@example.com")

assert user.username == "test"
assert user.email == "test@example.com"

def test_user_validation_succeeds_for_valid_data(): """Test validation succeeds for valid user data.""" user = User(username="test", email="test@example.com")

user.validate()

assert user.is_valid is True

def test_user_save_assigns_id(): """Test saving user assigns an ID.""" user = User(username="test", email="test@example.com")

user.save()

assert user.id is not None

... separate tests for other behaviors

Guideline: One logical assertion per test. Multiple assert statements are okay if they verify the same behavior.

✅ Acceptable:

def test_user_creation_sets_all_attributes(): """Test user creation sets all required attributes.""" user = User(username="test", email="test@example.com", age=30)

# Multiple assertions, but all verify the same behavior (creation)
assert user.username == "test"
assert user.email == "test@example.com"
assert user.age == 30
assert user.is_active is True  # Default value

7.5 Anti-Pattern: Not Testing Edge Cases

❌ Bad:

def test_divide(): """Only tests the happy path.""" assert divide(10, 2) == 5.0

✅ Good:

def test_divide_positive_numbers(): """Test division of positive numbers.""" assert divide(10, 2) == 5.0

def test_divide_negative_numbers(): """Test division with negative numbers.""" assert divide(-10, 2) == -5.0 assert divide(10, -2) == -5.0

def test_divide_by_zero_raises_error(): """Test division by zero raises ZeroDivisionError.""" with pytest.raises(ZeroDivisionError): divide(10, 0)

def test_divide_zero_by_number(): """Test zero divided by number returns zero.""" assert divide(0, 5) == 0.0

def test_divide_floats(): """Test division with floating point numbers.""" assert divide(10.5, 2.0) == 5.25

Common Edge Cases to Test:

  • Empty collections ([], {}, "")

  • None values

  • Zero

  • Negative numbers

  • Very large numbers

  • Boundary values (max/min)

  • Invalid input types

  • Special characters in strings

7.6 Anti-Pattern: Missing Negative Test Cases

❌ Bad:

def test_create_user(): """Only tests successful creation.""" user = user_service.create("test@example.com", "password123") assert user is not None

✅ Good:

def test_create_user_with_valid_data_succeeds(): """Test user creation succeeds with valid data.""" user = user_service.create("test@example.com", "password123") assert user is not None

def test_create_user_with_invalid_email_raises_error(): """Test user creation fails with invalid email.""" with pytest.raises(ValidationError): user_service.create("invalid-email", "password123")

def test_create_user_with_short_password_raises_error(): """Test user creation fails with short password.""" with pytest.raises(ValidationError): user_service.create("test@example.com", "pass")

def test_create_user_with_duplicate_email_raises_error(): """Test user creation fails with duplicate email.""" user_service.create("test@example.com", "password123")

with pytest.raises(DuplicateEmailError):
    user_service.create("test@example.com", "password456")

Why: Negative tests verify error handling and validation logic.

Best Practices Summary

  1. One Assertion Per Test (Guideline)

Guideline: Each test should verify one logical behavior. Multiple assert statements are acceptable if they all verify the same behavior.

✅ Good:

def test_user_registration_creates_complete_user(): """Test user registration creates user with all attributes.""" user = register_user("john@example.com", "password")

# All assertions verify the same behavior (successful registration)
assert user.email == "john@example.com"
assert user.is_active is True
assert user.created_at is not None

2. Test Names That Describe the Scenario

Pattern: test_<what><when><expected>

✅ Good:

def test_withdraw_with_insufficient_balance_raises_error(): """Test withdrawing more than balance raises InsufficientFundsError.""" pass

def test_login_with_valid_credentials_returns_token(): """Test login with valid credentials returns auth token.""" pass

  1. Fast Tests

Rule: Unit tests should run in milliseconds, entire suite in seconds.

Strategies:

  • Use in-memory databases (SQLite :memory: )

  • Mock external dependencies

  • Use fixtures with appropriate scopes

  • Avoid unnecessary I/O operations

  • Run slow tests separately (@pytest.mark.slow )

@pytest.mark.slow def test_full_database_migration(): """Slow test - run separately with pytest -m slow.""" pass

Run fast tests only

pytest -m "not slow"

  1. Independent Tests

Rule: Tests must not depend on each other or on execution order.

✅ Good:

@pytest.fixture(autouse=True) def reset_database(): """Reset database before each test.""" db.clear() yield db.clear()

def test_create_user(): """Each test starts with clean state.""" user = create_user("test") assert db.count() == 1

def test_delete_user(): """Independent of test_create_user.""" user = create_user("test") delete_user(user.id) assert db.count() == 0

  1. Repeatable Tests

Rule: Tests should produce the same results every time they run.

Avoid:

  • Uncontrolled randomness

  • System time dependencies

  • External API calls

  • Filesystem dependencies

  • Network dependencies

Use:

  • Mocked random with fixed seed

  • Mocked datetime

  • Mocked external services

  • Temporary directories/files

  • In-memory resources

  1. Self-Validating Tests

Rule: Tests should clearly pass or fail without human interpretation.

✅ Good:

def test_calculate_total(): """Test clearly passes or fails.""" total = calculate_total([10, 20, 30]) assert total == 60 # Clear pass/fail

❌ Bad:

def test_calculate_total(): """Requires human to interpret output.""" total = calculate_total([10, 20, 30]) print(f"Total: {total}") # No assertion - requires manual check

Examples

Example 1: Complete Test File

File: tests/unit/test_user_service.py

"""Tests for user service."""

import pytest from myapp.models import User from myapp.services import UserService from myapp.exceptions import ValidationError, DuplicateEmailError

@pytest.fixture def user_service(): """Provide user service instance.""" return UserService()

@pytest.fixture def sample_user_data(): """Provide sample valid user data.""" return { "email": "john@example.com", "password": "SecurePass123!", "username": "john_doe" }

class TestUserRegistration: """Tests for user registration functionality."""

def test_register_with_valid_data_creates_user(self, user_service, sample_user_data):
    """Test that registration with valid data creates a user."""
    # Arrange
    # (data provided by fixture)

    # Act
    user = user_service.register(**sample_user_data)

    # Assert
    assert user.email == sample_user_data["email"]
    assert user.username == sample_user_data["username"]
    assert user.id is not None
    assert user.is_active is True

def test_register_with_invalid_email_raises_error(self, user_service):
    """Test that invalid email raises ValidationError."""
    # Arrange
    invalid_data = {
        "email": "not-an-email",
        "password": "SecurePass123!",
        "username": "john_doe"
    }

    # Act &#x26; Assert
    with pytest.raises(ValidationError, match="Invalid email format"):
        user_service.register(**invalid_data)

def test_register_with_duplicate_email_raises_error(self, user_service, sample_user_data):
    """Test that duplicate email raises DuplicateEmailError."""
    # Arrange
    user_service.register(**sample_user_data)

    # Act &#x26; Assert
    with pytest.raises(DuplicateEmailError):
        user_service.register(**sample_user_data)

@pytest.mark.parametrize("password,should_fail", [
    ("short", True),  # Too short
    ("nodigits!", True),  # No digits
    ("NoSpecialChar1", True),  # No special char
    ("Valid1Pass!", False),  # Valid
    ("AnotherGood2@", False),  # Valid
])
def test_register_password_validation(self, user_service, password, should_fail):
    """Test password validation with various inputs."""
    # Arrange
    user_data = {
        "email": "test@example.com",
        "password": password,
        "username": "testuser"
    }

    # Act &#x26; Assert
    if should_fail:
        with pytest.raises(ValidationError):
            user_service.register(**user_data)
    else:
        user = user_service.register(**user_data)
        assert user is not None

class TestUserAuthentication: """Tests for user authentication functionality."""

@pytest.fixture
def registered_user(self, user_service, sample_user_data):
    """Provide a registered user for testing."""
    return user_service.register(**sample_user_data)

def test_login_with_valid_credentials_returns_token(
    self, user_service, registered_user, sample_user_data
):
    """Test that valid credentials return an auth token."""
    # Arrange
    email = sample_user_data["email"]
    password = sample_user_data["password"]

    # Act
    token = user_service.login(email, password)

    # Assert
    assert token is not None
    assert isinstance(token, str)
    assert len(token) > 0

def test_login_with_invalid_password_raises_error(
    self, user_service, registered_user, sample_user_data
):
    """Test that invalid password raises AuthenticationError."""
    # Arrange
    email = sample_user_data["email"]
    wrong_password = "WrongPassword123!"

    # Act &#x26; Assert
    with pytest.raises(AuthenticationError):
        user_service.login(email, wrong_password)

def test_login_with_nonexistent_user_raises_error(self, user_service):
    """Test that non-existent user raises AuthenticationError."""
    # Arrange
    email = "nonexistent@example.com"
    password = "Password123!"

    # Act &#x26; Assert
    with pytest.raises(AuthenticationError):
        user_service.login(email, password)

class TestUserEmailNotification: """Tests for user email notification functionality."""

def test_register_sends_welcome_email(self, user_service, sample_user_data, mocker):
    """Test that registration sends welcome email."""
    # Arrange
    mock_email = mocker.patch("myapp.services.EmailService.send")

    # Act
    user = user_service.register(**sample_user_data)

    # Assert
    mock_email.assert_called_once_with(
        to=sample_user_data["email"],
        subject="Welcome to MyApp!",
        template="welcome",
        context={"username": user.username}
    )

def test_register_does_not_send_email_on_failure(
    self, user_service, sample_user_data, mocker
):
    """Test that failed registration does not send email."""
    # Arrange
    mock_email = mocker.patch("myapp.services.EmailService.send")
    invalid_data = {**sample_user_data, "email": "invalid-email"}

    # Act
    with pytest.raises(ValidationError):
        user_service.register(**invalid_data)

    # Assert
    mock_email.assert_not_called()

Example 2: Testing with Fixtures and Factories

"""Example using fixtures and factories for test data."""

import pytest from myapp.models import User, Post, Comment

@pytest.fixture def user_factory(db): """Provide factory for creating test users.""" created_users = []

def _create_user(**kwargs):
    defaults = {
        "username": f"user_{len(created_users)}",
        "email": f"user{len(created_users)}@example.com",
        "is_active": True
    }
    defaults.update(kwargs)
    user = User(**defaults)
    db.save(user)
    created_users.append(user)
    return user

yield _create_user

# Cleanup
for user in created_users:
    db.delete(user)

@pytest.fixture def post_factory(db, user_factory): """Provide factory for creating test posts.""" created_posts = []

def _create_post(**kwargs):
    if "author" not in kwargs:
        kwargs["author"] = user_factory()

    defaults = {
        "title": f"Post {len(created_posts)}",
        "content": "Test content"
    }
    defaults.update(kwargs)
    post = Post(**defaults)
    db.save(post)
    created_posts.append(post)
    return post

yield _create_post

# Cleanup
for post in created_posts:
    db.delete(post)

def test_user_can_create_multiple_posts(user_factory, post_factory): """Test that user can create multiple posts.""" # Arrange user = user_factory(username="author")

# Act
post1 = post_factory(author=user, title="First Post")
post2 = post_factory(author=user, title="Second Post")

# Assert
assert post1.author == user
assert post2.author == user
assert user.posts.count() == 2

def test_post_without_author_creates_default_user(post_factory): """Test that post without author gets default user.""" # Act post = post_factory(title="Test Post")

# Assert
assert post.author is not None
assert post.author.username.startswith("user_")

Example 3: Integration Test with Database

"""Integration tests with real database."""

import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from myapp.models import Base, User from myapp.repositories import UserRepository

@pytest.fixture(scope="module") def engine(): """Create in-memory SQLite engine for testing.""" engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) yield engine Base.metadata.drop_all(engine)

@pytest.fixture def db_session(engine): """Provide database session with transaction rollback.""" connection = engine.connect() transaction = connection.begin() Session = sessionmaker(bind=connection) session = Session()

yield session

session.close()
transaction.rollback()
connection.close()

@pytest.fixture def user_repository(db_session): """Provide user repository instance.""" return UserRepository(session=db_session)

def test_create_user_persists_to_database(user_repository): """Test that created user is persisted to database.""" # Arrange user_data = { "username": "testuser", "email": "test@example.com" }

# Act
user = user_repository.create(**user_data)
retrieved_user = user_repository.find_by_id(user.id)

# Assert
assert retrieved_user is not None
assert retrieved_user.username == "testuser"
assert retrieved_user.email == "test@example.com"

def test_find_by_email_returns_correct_user(user_repository): """Test finding user by email returns correct user.""" # Arrange user1 = user_repository.create(username="user1", email="user1@example.com") user2 = user_repository.create(username="user2", email="user2@example.com")

# Act
found_user = user_repository.find_by_email("user2@example.com")

# Assert
assert found_user is not None
assert found_user.id == user2.id
assert found_user.username == "user2"

Templates

Template 1: Basic Unit Test

Located at: templates/unit_test_template.py

Purpose: Template for basic unit tests using AAA pattern.

Usage:

  • Copy template to tests/unit/test_[module_name].py

  • Replace [MODULE_NAME] with actual module name

  • Replace [function/class] with code under test

  • Add test cases following the pattern

See: templates/unit_test_template.py

Template 2: Integration Test with Database

Located at: templates/integration_test_template.py

Purpose: Template for integration tests with database fixtures.

Usage:

  • Copy template to tests/integration/test_[feature_name].py

  • Configure database engine for your database

  • Add test cases using db_session fixture

See: templates/integration_test_template.py

Template 3: Test with Mocking

Located at: templates/mock_test_template.py

Purpose: Template for tests using mocks and patches.

Usage:

  • Copy template to appropriate test directory

  • Replace mock targets with actual dependencies

  • Add verification assertions

See: templates/mock_test_template.py

Decision Trees

When to Mock vs Test Real Implementation

Is the dependency external (API, database, file system)? ├─ Yes → Mock it │ └─ Example: mock requests.get(), EmailService.send() └─ No → Is it slow (>100ms)? ├─ Yes → Mock it │ └─ Example: mock expensive computations, large file processing └─ No → Is it non-deterministic (random, time)? ├─ Yes → Mock it │ └─ Example: mock datetime.now(), random.randint() └─ No → Is it your internal business logic? ├─ Yes → Test it directly (don't mock) │ └─ Example: test calculation functions, validators └─ No → Is it a framework/library? ├─ Yes → Don't test it (trust the framework) └─ No → Consider mocking if needed for isolation

Choosing Fixture Scope

Does the fixture need to be fresh for each test? ├─ Yes → Use scope="function" (default) │ └─ Example: test data, mutable objects └─ No → Is the fixture expensive to create? ├─ Yes → Is it stateless/read-only? │ ├─ Yes → Use scope="module" or "session" │ │ └─ Example: database connection, API client │ └─ No → Use scope="function" with cleanup │ └─ Example: database with data └─ No → Use scope="function" for simplicity

Test Organization Strategy

What are you testing? ├─ Single function/method with no dependencies │ └─ Use standalone test function │ └─ Example: def test_calculate_sum() ├─ Multiple related functions/methods │ └─ Use test class for grouping │ └─ Example: class TestUserAuthentication ├─ Tests sharing setup/teardown │ └─ Use test class with fixtures │ └─ Example: class TestDatabaseOperations with setup fixtures └─ Different test types (unit/integration/e2e) └─ Use separate directories └─ Example: tests/unit/, tests/integration/, tests/e2e/

Common Pitfalls

Pitfall 1: Mocking Too Much

Problem: Over-mocking makes tests brittle and defeats the purpose of testing.

Why it happens: Desire for fast tests or lack of understanding of what to mock.

How to avoid:

  • Only mock external dependencies and slow operations

  • Test real implementation of your business logic

  • Use integration tests for testing components together

Example:

❌ Bad:

def test_user_service_register(mocker): """Over-mocked test that doesn't test anything real.""" mock_user = mocker.Mock() mock_validator = mocker.patch("myapp.validators.EmailValidator") mock_hasher = mocker.patch("myapp.security.hash_password") mock_repo = mocker.patch("myapp.repositories.UserRepository")

# This test doesn't test any real code!
service = UserService()
service.register("test@example.com", "password")

mock_validator.validate.assert_called_once()
mock_hasher.assert_called_once()
mock_repo.save.assert_called_once()

✅ Good:

def test_user_service_register(mocker): """Test real logic, mock only external dependencies.""" # Only mock external dependencies (database, email) mock_email = mocker.patch("myapp.services.EmailService.send")

# Test real validation, hashing, and service logic
service = UserService()
user = service.register("test@example.com", "SecurePass123!")

# Verify real behavior
assert user.email == "test@example.com"
assert user.password_hash != "SecurePass123!"  # Password was hashed
mock_email.assert_called_once()  # Email was sent

Pitfall 2: Testing Private Methods

Problem: Tests are coupled to implementation details and break on refactoring.

Why it happens: Misconception that 100% coverage requires testing private methods.

How to avoid:

  • Test public API/interface only

  • Private methods are tested indirectly through public methods

  • If a private method seems complex enough to test directly, it might deserve to be a separate class

Example:

❌ Bad:

class UserService: def register(self, email, password): self._validate_email(email) self._validate_password(password) return self._create_user(email, password)

def _validate_email(self, email):
    # Private validation logic
    pass

def _create_user(self, email, password):
    # Private creation logic
    pass

DON'T DO THIS

def test_validate_email(): """Testing private method directly.""" service = UserService() service._validate_email("test@example.com") # Bad!

✅ Good:

Test the public API

def test_register_with_invalid_email_raises_error(): """Test registration validates email (tests _validate_email indirectly).""" service = UserService()

with pytest.raises(ValidationError):
    service.register("invalid-email", "SecurePass123!")

def test_register_with_valid_data_creates_user(): """Test registration creates user (tests _create_user indirectly).""" service = UserService()

user = service.register("test@example.com", "SecurePass123!")

assert user.email == "test@example.com"

Pitfall 3: Shared State Between Tests

Problem: Tests fail or pass depending on execution order.

Why it happens: Using module-level or class-level mutable state without proper cleanup.

How to avoid:

  • Use fixtures with proper scope

  • Clean up state in fixtures (use yield for teardown)

  • Avoid module-level globals

  • Use autouse=True fixtures for necessary cleanup

Example:

❌ Bad:

Module-level shared state

_test_users = []

def test_create_user(): """Test depends on _test_users being empty.""" user = User(username="test") _test_users.append(user) assert len(_test_users) == 1 # Fails if other tests ran first!

def test_delete_user(): """Test depends on test_create_user.""" _test_users.pop() assert len(_test_users) == 0

✅ Good:

@pytest.fixture def user_list(): """Provide fresh list for each test.""" users = [] yield users users.clear() # Cleanup (though not needed with function scope)

def test_create_user(user_list): """Test with isolated state.""" user = User(username="test") user_list.append(user) assert len(user_list) == 1

def test_delete_user(user_list): """Test with isolated state.""" user = User(username="test") user_list.append(user) user_list.pop() assert len(user_list) == 0

Pitfall 4: Not Cleaning Up Resources

Problem: Tests leave behind files, database records, or open connections.

Why it happens: Forgetting to add cleanup code or not using fixtures properly.

How to avoid:

  • Use fixtures with yield for setup/teardown

  • Use context managers

  • Use temp directories for file tests

  • Use transaction rollback for database tests

Example:

❌ Bad:

def test_file_processing(): """Test leaves file behind.""" with open("test_file.txt", "w") as f: f.write("test data")

result = process_file("test_file.txt")
assert result == "processed"
# File left behind!

✅ Good:

import tempfile import os

@pytest.fixture def test_file(): """Provide temporary test file.""" fd, path = tempfile.mkstemp(suffix=".txt")

# Write test data
with os.fdopen(fd, "w") as f:
    f.write("test data")

yield path

# Cleanup
if os.path.exists(path):
    os.remove(path)

def test_file_processing(test_file): """Test with automatic cleanup.""" result = process_file(test_file) assert result == "processed" # File automatically cleaned up

Pitfall 5: Unclear Test Failure Messages

Problem: When test fails, it's not clear what went wrong.

Why it happens: Using bare assertions without context or descriptive messages.

How to avoid:

  • Use descriptive test names

  • Add assertion messages for complex checks

  • Use pytest's assertion rewriting (automatic for simple assertions)

  • Use pytest.raises() with match parameter

Example:

❌ Bad:

def test_user(): """Vague test name.""" u = User("test@example.com") assert u.email == "test@example.org" # Failure message unclear

When this fails:

assert 'test@example.com' == 'test@example.org'

✅ Good:

def test_user_email_is_stored_correctly(): """Descriptive test name explains what's being tested.""" # Arrange email = "test@example.com"

# Act
user = User(email)

# Assert
assert user.email == email, f"Expected user email to be {email}, got {user.email}"

When this fails:

AssertionError: Expected user email to be test@example.com, got test@example.org

For Exceptions:

✅ Good:

def test_invalid_email_raises_validation_error(): """Test that invalid email raises ValidationError with helpful message.""" with pytest.raises(ValidationError, match="Invalid email format"): User("not-an-email")

Checklist

Use this checklist during code reviews to verify test quality:

Test File Organization

  • Test files named test_*.py or *_test.py

  • Tests organized in logical directories (unit/ , integration/ , e2e/ )

  • Shared fixtures in conftest.py at appropriate levels

  • One test file per module/class being tested

Test Function/Class Naming

  • Test functions start with test_

  • Test classes start with Test

  • Names describe the scenario: test_<what><when><expected>

  • Names are clear and self-documenting

Test Structure

  • Tests follow AAA or Given-When-Then pattern

  • Clear separation between Arrange, Act, Assert

  • Each test verifies one logical behavior

  • Tests are independent and can run in any order

Fixtures

  • Appropriate fixture scope selected (function/class/module/session)

  • Fixtures have descriptive names

  • Fixtures clean up resources (using yield)

  • Fixture dependencies are logical and clear

  • autouse only used when necessary

Mocking and Patching

  • Only external dependencies are mocked

  • Internal business logic is tested directly

  • Patches target where object is used, not where it's defined

  • Mock assertions verify expected behavior

  • Mocks are reset between tests (automatic with mocker fixture)

Test Coverage

  • Critical paths have 100% coverage

  • Overall coverage meets project threshold (80%+)

  • Coverage report excludes irrelevant files (migrations, etc.)

  • Business logic is thoroughly tested

  • Edge cases are covered

Assertions

  • Tests have at least one assertion

  • Assertions are specific and meaningful

  • Exception tests use pytest.raises()

  • Assertion messages provided for complex checks

  • No bare assert without verification

Test Quality

  • No testing of implementation details

  • No overly complex test setup

  • Tests are not flaky (deterministic)

  • Edge cases are tested

  • Negative test cases are included

  • Tests are fast (unit tests < 100ms)

Parametrization

  • @pytest.mark.parametrize used for multiple similar test cases

  • Test IDs provided for clarity (using pytest.param )

  • Parametrized tests cover full range of inputs

Code Quality

  • Tests follow PEP 8 style guide

  • Tests have docstrings describing what they test

  • No code duplication (use fixtures/helpers)

  • Imports are organized and minimal

  • No commented-out code

Documentation

  • README explains how to run tests

  • Complex test scenarios are documented

  • Custom markers are registered and documented

  • Fixture purposes are clear from docstrings

Tools and Commands

Running Tests

Run all tests

pytest

Run specific test file

pytest tests/unit/test_user_service.py

Run specific test function

pytest tests/unit/test_user_service.py::test_register_with_valid_data

Run specific test class

pytest tests/unit/test_user_service.py::TestUserRegistration

Run tests matching pattern

pytest -k "registration"

Run tests with specific marker

pytest -m "slow"

Run tests excluding marker

pytest -m "not slow"

Verbose output

pytest -v

Show print statements

pytest -s

Stop after first failure

pytest -x

Run last failed tests only

pytest --lf

Run failed tests first, then rest

pytest --ff

Show slowest tests

pytest --durations=10

Coverage Commands

Run with coverage

pytest --cov=myapp

HTML report

pytest --cov=myapp --cov-report=html

Terminal report with missing lines

pytest --cov=myapp --cov-report=term-missing

XML report (for CI)

pytest --cov=myapp --cov-report=xml

Fail if coverage below threshold

pytest --cov=myapp --cov-fail-under=80

Multiple reports

pytest --cov=myapp --cov-report=html --cov-report=term --cov-report=xml

Pytest Configuration

pytest.ini:

[pytest]

Test discovery patterns

python_files = test_.py _test.py python_classes = Test python_functions = test_

Default options

addopts = -v --strict-markers --tb=short --cov=myapp --cov-report=term-missing --cov-fail-under=80

Test paths

testpaths = tests

Markers

markers = slow: marks tests as slow (deselect with '-m "not slow"') integration: marks tests as integration tests unit: marks tests as unit tests e2e: marks tests as end-to-end tests

Coverage options

[coverage:run] source = myapp omit = /tests/ /migrations/ /venv/ /pycache/

[coverage:report] exclude_lines = pragma: no cover def repr raise AssertionError raise NotImplementedError if name == .main.: if TYPE_CHECKING: @abstractmethod

Useful Pytest Plugins

Install common plugins

pip install pytest-cov # Coverage integration pip install pytest-mock # Mocking integration pip install pytest-xdist # Parallel test execution pip install pytest-django # Django integration pip install pytest-asyncio # Async test support pip install pytest-timeout # Test timeouts pip install pytest-benchmark # Performance benchmarking

Run tests in parallel

pytest -n auto # Uses all CPU cores pytest -n 4 # Uses 4 workers

Set test timeout

pytest --timeout=10 # 10 seconds per test

Related Skills

  • uncle-duke-python: Python code review agent that uses this skill as reference

  • agent-skill-templates: Templates for creating new skills

References

Official Documentation

  • pytest Documentation

  • unittest.mock Documentation

  • pytest-cov Documentation

  • pytest-mock Documentation

Best Practices Guides

  • Effective Python Testing with pytest (Book)

  • Python Testing Best Practices

  • AAA Pattern

  • Given-When-Then

PEPs Referenced

  • PEP 8 - Style Guide for Python Code

  • PEP 20 - The Zen of Python

  • PEP 484 - Type Hints

Version: 1.0 Last Updated: 2025-12-24 Maintainer: Development Team

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

python-type-hints-guide

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

java-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
General

spring-framework-patterns

No summary provided by upstream source.

Repository SourceNeeds Review