Pytest Testing: From First Test to Advanced Fixtures and Mocking

Python Testing With pytest: From First Test to Advanced Fixtures and Mocking
4 views

pytest is the testing framework that makes Python tests a joy to write — if you know its features. Most developers use pytest as a basic test runner and miss fixtures with scope, parametrize for data-driven tests, conftest.py for shared setup, and the patching patterns that make external dependencies testable. This guide focuses on pytest testing, with the production trade-offs senior engineers need to make sound decisions.

⚡ TL;DR: Fixtures for reusable setup/teardown with scope control. @pytest.mark.parametrize for data-driven tests. conftest.py for shared fixtures across test files. unittest.mock.patch for external dependencies. pytest-cov for coverage. pytest.raises for exception testing.

Fixtures — reusable test setup — pytest testing

import pytest

# Function scope (default): new fixture per test
@pytest.fixture
def user_data():
    return {'name':'Alice','email':'alice@example.com','age':30}

# Module scope: created once per module
@pytest.fixture(scope='module')
def db_connection():
    conn = create_test_db()
    yield conn  # Provide to test
    conn.close()  # Cleanup after ALL tests in module

# Session scope: created once for entire test session
@pytest.fixture(scope='session')
def app_client():
    from myapp import create_app
    app = create_app({'TESTING':True,'DB':'sqlite:///:memory:'})
    with app.test_client() as client:
        yield client

# Using fixtures:
def test_create_user(db_connection, user_data):
    result = db_connection.create_user(**user_data)
    assert result.id is not None
    assert result.email == user_data['email']

parametrize — data-driven tests

@pytest.mark.parametrize('email,valid', [
    ('alice@example.com', True),
    ('bob@test.co.uk',    True),
    ('notanemail',        False),
    ('@nodomain.com',     False),
    ('',                  False),
    ('a' * 250 + '@example.com', False),  # Too long
])
def test_email_validation(email, valid):
    assert validate_email(email) == valid

# Multiple parameters:
@pytest.mark.parametrize('a,b,expected', [
    (1, 2, 3), (0, 0, 0), (-1, 1, 0), (100, -1, 99)
])
def test_add(a, b, expected):
    assert add(a, b) == expected

# Generate 6 test cases from one parametrize — efficient!

Mocking external dependencies

from unittest.mock import patch, Mock, AsyncMock

def test_send_email_on_user_creation():
    with patch('myapp.email_service.send') as mock_send:
        mock_send.return_value = {'status':'sent','id':'msg-123'}
        result = create_user({'name':'Alice','email':'alice@example.com'})
        assert result.id is not None
        mock_send.assert_called_once_with(
            to='alice@example.com',
            template='welcome'
        )

# Async mocking:
async def test_fetch_user():
    with patch('myapp.http_client.get', new_callable=AsyncMock) as mock_get:
        mock_get.return_value = Mock(json=AsyncMock(return_value={'id':1,'name':'Alice'}))
        user = await fetch_user(1)
        assert user['name'] == 'Alice'

conftest.py and advanced patterns

# conftest.py — shared across test files, no import needed
import pytest
from myapp import create_app
from myapp.database import db

@pytest.fixture(scope='session')
def app():
    app = create_app({'TESTING':True,'SQLALCHEMY_DATABASE_URI':'sqlite:///:memory:'})
    with app.app_context():
        db.create_all()
        yield app

@pytest.fixture
def client(app):
    return app.test_client()

@pytest.fixture(autouse=True)  # Applied to ALL tests automatically
def reset_db(app):
    yield
    with app.app_context():
        db.session.rollback()  # Reset after each test

# pytest.ini — configure coverage:
[pytest]
addopts = --cov=myapp --cov-report=term-missing --cov-fail-under=80
  • ✅ Function scope fixtures for test isolation, session scope for expensive setup
  • ✅ parametrize for data-driven tests — test many cases in one function
  • ✅ conftest.py for shared fixtures — no import needed
  • ✅ autouse=True for fixtures that apply to all tests (DB reset)
  • ✅ AsyncMock for async functions
  • ❌ Never use real external services in unit tests — always mock
  • ❌ Never share mutable state between tests — causes order-dependent failures

External reference: pytest documentation.

Recommended Reading

→ Designing Data-Intensive Applications — The bible of distributed systems and production engineering at scale.

→ The Pragmatic Programmer — Timeless engineering wisdom every senior developer needs.

Affiliate links. We earn a small commission at no extra cost to you.

Related on CheatCoders: unit testing best practices that catch production bugs, Python context managers, and Python type hints.

Free Weekly Newsletter

🚀 Join 2,000+ Senior Developers

Get expert-level JavaScript, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.

✓ No spam✓ Unsubscribe anytime✓ Expert-level only

Related: Python Dataclasses: 10 Advanced Features That Make __init__ Obsolete · Lambda Test Events as Contracts: Schema-Locked Fixtures Across Teams · Python Decorators Explained: From Simple Wrappers to Production Patterns

Related: Python asyncio vs Threading vs Multiprocessing: When to Use Each · JavaScript Promises: The Complete Guide From Basics to Advanced Patterns · Python Dictionary Methods and Tricks: The Complete Guide

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.