Mathew K Analytics

Lesson 4 · Pytest deep dive

pytest Tutorial #4: Mocking & Monkeypatch

On testing real Python code with pytest. unittest.mock basics and the built-in monkeypatch fixture. Let's get into it. Mock objects are fake, controllable…

⬇ Download notebook

📓 Full notebook

Download .ipynb

pytest Deep-Dive, Video 4: Mocking and monkeypatch#

  • Video four of an eighteen-part series on testing real Python code with pytest.
  • unittest.mock basics and the built-in monkeypatch fixture.
  • 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 Mock - the External-Dependency Problem#

from unittest.mock import Mock
fake_response = Mock()
fake_response.status_code = 200
fake_response.json.return_value = {'ok': True}
print(fake_response.status_code)
print(fake_response.json())
200
{'ok': True}

Part 3: Mock Call Tracking#

code = '''
from unittest.mock import Mock

def send_notification(sender, message):
    sender.send(message)

def test_send_notification_calls_sender():
    fake_sender = Mock()
    send_notification(fake_sender, 'hello')
    assert fake_sender.send.called
    fake_sender.send.assert_called_once_with('hello')
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmp1g6u2ioo_test.py::test_send_notification_calls_sender PASSED [100%]

============================== 1 passed in 0.18s ==============================
0

Part 4: monkeypatch - Replacing a Function#

code = '''
import random

def roll_die():
    return random.randint(1, 6)

def test_roll_die_is_patched(monkeypatch):
    monkeypatch.setattr(random, 'randint', lambda a, b: 4)
    assert roll_die() == 4
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmp97m08_d__test.py::test_roll_die_is_patched PASSED [100%]

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

Part 5: monkeypatch Automatically Undoes Itself#

code = '''
import random

def roll_die():
    return random.randint(1, 6)

def test_patched_first(monkeypatch):
    monkeypatch.setattr(random, 'randint', lambda a, b: 4)
    assert roll_die() == 4

def test_unpatched_after(monkeypatch):
    result = roll_die()
    assert 1 <= result <= 6
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpd7lsa4gr_test.py::test_patched_first PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpd7lsa4gr_test.py::test_unpatched_after PASSED [100%]

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

Part 6: monkeypatch and Environment Variables#

code = '''
import os

def get_api_url():
    return os.environ.get('API_URL', 'https://default.example.com')

def test_get_api_url_uses_env(monkeypatch):
    monkeypatch.setenv('API_URL', 'https://staging.example.com')
    assert get_api_url() == 'https://staging.example.com'

def test_get_api_url_falls_back(monkeypatch):
    monkeypatch.delenv('API_URL', raising=False)
    assert get_api_url() == 'https://default.example.com'
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpaogb77lt_test.py::test_get_api_url_uses_env PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpaogb77lt_test.py::test_get_api_url_falls_back PASSED [100%]

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

Part 7: unittest.mock.patch as a Context Manager#

code = '''
from unittest.mock import patch
import datetime

def get_year():
    return datetime.date.today().year

def test_get_year_patched():
    fixed_date = datetime.date(2030, 1, 1)
    with patch('datetime.date') as mock_date:
        mock_date.today.return_value = fixed_date
        assert get_year() == 2030
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmpy4ys72i6_test.py::test_get_year_patched PASSED [100%]

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

Part 8: A Real Pattern - Testing Code That Calls a Network Function#

code = '''
def fetch_weather(city):
    raise RuntimeError("real network call, should never run in tests")

def describe_weather(city, fetch=fetch_weather):
    data = fetch(city)
    return f"{city}: {data['condition']}, {data['temp_c']} C"

def test_describe_weather_with_fake_fetch():
    def fake_fetch(city):
        return {'condition': 'sunny', 'temp_c': 22}
    result = describe_weather('Perth', fetch=fake_fetch)
    assert result == 'Perth: sunny, 22 C'
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmpiq7o9kj9_test.py::test_describe_weather_with_fake_fetch PASSED [100%]

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

Wrap-Up: What You Learned#

  • Mock objects are fake, controllable stand-ins for real dependencies like network calls or external services.
  • Every Mock records how it was called; called, call_count, and assert_called_with verify real interactions.
  • monkeypatch is a built-in pytest fixture requiring no import, used like any other fixture parameter.
  • monkeypatch.setattr replaces an attribute or function; changes automatically revert after each test.
  • monkeypatch.setenv and delenv safely control environment variables for the duration of one test.
  • unittest.mock.patch offers a context-manager alternative, scoped to a with block.
  • Injecting a dependency as a default parameter makes it trivial to swap in a fake for fast, offline tests.
  • That wraps up mocking and monkeypatch. Next up: Markers.

Found this useful?

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