Mathew K Analytics

Lesson 2 · Pytest deep dive

pytest Tutorial #2: Fixtures Basics

On testing real Python code with pytest. Fixtures: real reusable setup and teardown, scope, and composition. Let's get into it. @pytest.fixture marks a…

⬇ Download notebook

📓 Full notebook

Download .ipynb

pytest Deep-Dive, Video 2: Fixtures Basics#

  • Video two of an eighteen-part series on testing real Python code with pytest.
  • Fixtures: real reusable setup and teardown, scope, and composition.
  • Let's get into it.

Part 1: The Helper From Last Time#

import pytest, tempfile, os
def run_pytest(code_str, args=None):
    with tempfile.NamedTemporaryFile(mode='w', suffix='_test.py', delete=False) as f:
        f.write(code_str)
        path = f.name
    result = pytest.main([path, *(args or [])])
    os.unlink(path)
    return result

Part 2: Why Fixtures - the Manual-Setup Problem#

code = '''
def test_manual_one():
    data = {'a': 1, 'b': 2}
    assert sum(data.values()) == 3

def test_manual_two():
    data = {'a': 1, 'b': 2}
    assert list(data.keys()) == ['a', 'b']
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpppc2ih6d_test.py::test_manual_one PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpppc2ih6d_test.py::test_manual_two PASSED [100%]

============================== 2 passed in 0.22s ==============================
0

Part 3: A Basic Fixture With @pytest.fixture#

code = '''
import pytest

@pytest.fixture
def sample_data():
    return {'a': 1, 'b': 2}

def test_fixture_one(sample_data):
    assert sum(sample_data.values()) == 3

def test_fixture_two(sample_data):
    assert list(sample_data.keys()) == ['a', 'b']
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmp5efuoxhz_test.py::test_fixture_one PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp5efuoxhz_test.py::test_fixture_two PASSED [100%]

============================== 2 passed in 0.11s ==============================
0

Part 4: yield Fixtures - Setup and Teardown#

code = '''
import pytest

events = []

@pytest.fixture
def tracked_resource():
    events.append('setup')
    resource = {'open': True}
    yield resource
    events.append('teardown')
    resource['open'] = False

def test_resource_is_open(tracked_resource):
    assert tracked_resource['open'] is True
    events.append('test ran')
'''
exit_code = run_pytest(code, ['-v', '--no-header', '-s'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmpy5rfgotm_test.py::test_resource_is_open PASSED

============================== 1 passed in 0.10s ==============================
0

Part 5: Fixture Scope - function, module, session#

code = '''
import pytest

call_count = {'function': 0, 'module': 0}

@pytest.fixture
def function_scoped():
    call_count['function'] += 1
    return call_count['function']

@pytest.fixture(scope='module')
def module_scoped():
    call_count['module'] += 1
    return call_count['module']

def test_one(function_scoped, module_scoped):
    assert function_scoped == 1
    assert module_scoped == 1

def test_two(function_scoped, module_scoped):
    assert function_scoped == 2
    assert module_scoped == 1
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpnwgm01c4_test.py::test_one PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpnwgm01c4_test.py::test_two PASSED [100%]

============================== 2 passed in 0.11s ==============================
0

Part 6: Fixture Composition - a Fixture Using Another Fixture#

code = '''
import pytest

@pytest.fixture
def base_config():
    return {'env': 'test', 'debug': True}

@pytest.fixture
def client_config(base_config):
    cfg = dict(base_config)
    cfg['client_id'] = 'demo-client'
    return cfg

def test_base_config(base_config):
    assert base_config['env'] == 'test'

def test_client_config(client_config):
    assert client_config['env'] == 'test'
    assert client_config['client_id'] == 'demo-client'
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmp1u3ev2so_test.py::test_base_config PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp1u3ev2so_test.py::test_client_config PASSED [100%]

============================== 2 passed in 0.11s ==============================
0

Part 7: A Real Pattern - an In-Memory Fake Database Fixture#

code = '''
import pytest

@pytest.fixture
def fake_db():
    db = {}
    yield db
    db.clear()

def add_user(db, name):
    user_id = len(db) + 1
    db[user_id] = {'name': name}
    return user_id

def test_add_user_creates_entry(fake_db):
    uid = add_user(fake_db, 'Alice')
    assert uid == 1
    assert fake_db[1]['name'] == 'Alice'

def test_fake_db_starts_empty_every_test(fake_db):
    assert fake_db == {}
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpz2ffcyin_test.py::test_add_user_creates_entry PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpz2ffcyin_test.py::test_fake_db_starts_empty_every_test PASSED [100%]

============================== 2 passed in 0.10s ==============================
0

Wrap-Up: What You Learned#

  • @pytest.fixture marks a function as reusable setup that tests request by matching parameter name.
  • A plain return fixture just hands back a value; every requesting test gets it as an argument.
  • A yield fixture splits into setup (before yield) and teardown (after yield), run around each test.
  • scope controls how often a fixture reruns: function (default), module, or session.
  • A fixture can request another fixture as its own parameter; pytest resolves the whole chain in order.
  • Fixtures avoid duplicating setup code and keep tests isolated with fresh state each time.
  • That wraps up fixtures basics. Next up: Parametrize.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.