🎯

python-testing-standards

🎯Skill

from clostaunau/holiday-card

VibeIndex|
What it does

Enforces Python testing best practices, providing comprehensive guidelines for writing high-quality, maintainable pytest test suites with code coverage and mocking strategies.

python-testing-standards

Installation

Install skill:
npx skills add https://github.com/clostaunau/holiday-card --skill python-testing-standards
3
AddedJan 25, 2026

Skill Details

SKILL.md

Comprehensive Python testing best practices, pytest conventions, test structure patterns (AAA, Given-When-Then), fixture usage, mocking strategies, code coverage standards, and common anti-patterns. Essential reference for code reviews, test writing, and ensuring high-quality Python test suites with pytest, unittest.mock, and pytest-cov.

Overview

# 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:

```python

# File: test_user_service.py

# File: test_authentication.py

# File: user_service_test.py # acceptable but less common

```

❌ Bad:

```python

# 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___

βœ… Good:

```python

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:

```python

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:

```python

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:

```python

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:

```python

"""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:

  1. Arrange: Set up test data and preconditions
  2. Act: Execute the behavior being tested
  3. Assert: Verify the expected outcome

βœ… Good:

```python

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):

```python

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:

```python

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:

  1. Given: Preconditions and setup (same as Arrange)
  2. When: Action being tested (same as Act)
  3. Then: Expected outcome (same as Assert)

βœ… Good:

```python

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:

```python

@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:

```python

# 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:

```python

@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:

```python

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:

```python

@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:

```python

@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:

```python

@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:

```python

@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:

```python

@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:

```python

# 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:

```python

@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:

```python

@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:

```python

"""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:

```python

@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:

```python

@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:

```python

@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:

```python

@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):

```python

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):

```python

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:

```python

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:

```python

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:

```python

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:

```python

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:

```python

# 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:

```python

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:

```python

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:

```python

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:

```python

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:

```python

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:

```python

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:

```ini

[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:

```bash

# 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:

```python

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:

```python

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:

```python

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:

```python

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:

```python

# 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:

```python

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:

```python

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:

```python

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

```

#### 6.3 Coverage Tools

pytest-cov:

```bash

# 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):

```bash

# 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:

```ini

[coverage:run]

branch = True # Enable branch coverage (not just line coverage)

```

```python

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:

```python

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:

```python

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:

```python

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:

```python

@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:

```python

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 < 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:

```python

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:

```python

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:

```python

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:

```python

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:

```python

def test_divide():

"""Only tests the happy path."""

assert divide(10, 2) == 5.0

```

βœ… Good:

```python

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:

```python

def test_create_user():

"""Only tests successful creation."""

user = user_service.create("test@example.com", "password123")

assert user is not None

```

βœ… Good:

```python

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:

```python

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___

βœ… Good:

```python

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

```

3. 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)

```python

@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"

```

4. Independent Tests

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

βœ… Good:

```python

@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

```

5. 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

6. Self-Validating Tests

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

βœ… Good:

```python

def test_calculate_total():

"""Test clearly passes or fails."""

total = calculate_total([10, 20, 30])

assert total == 60 # Clear pass/fail

```

❌ Bad:

```python

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

```python

"""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 & 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 & 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 & 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 & 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 & 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

```python

"""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_user