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

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

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.

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

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.

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

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 Reply