Mathew K Analytics

Lesson 11 · Pytest deep dive

pytest Tutorial #11: Plugins — Extending pytest

On testing real Python code with pytest. pytest-mock, pytest-cov, and pytest-xdist: three real popular plugins. Let's get into it. A pytest plugin is a…

⬇ Download notebook

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

pytest Deep-Dive, Video 11: Plugins#

  • Video eleven of an eighteen-part series on testing real Python code with pytest.
  • pytest-mock, pytest-cov, and pytest-xdist: three real popular plugins.
  • Let's get into it.

Part 1: What a pytest Plugin Actually Is#

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: pytest-mock - the mocker Fixture#

code = '''
def send_email(sender, to, subject):
    sender.send(to, subject)

def test_send_email(mocker):
    fake_sender = mocker.Mock()
    send_email(fake_sender, 'a@b.com', 'hi')
    fake_sender.send.assert_called_once_with('a@b.com', 'hi')
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 1 item

..\..\..\Users\makmw\AppData\Local\Temp\tmpxfyanui8_test.py::test_send_email ERROR [100%]

=================================== ERRORS ====================================
______________________ ERROR at setup of test_send_email ______________________
file C:\Users\makmw\AppData\Local\Temp\tmpxfyanui8_test.py, line 5
  def test_send_email(mocker):
E       fixture 'mocker' not found
>       available fixtures: _session_faker, anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, dash_br, dash_duo, dash_duo_mp, dash_multi_process_server, dash_process_server, dash_thread_server, dashjl, dashjl_server, dashr, dashr_server, diskcache_manager, doctest_namespace, faker, free_tcp_port, free_tcp_port_factory, free_udp_port, free_udp_port_factory, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, subtests, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

C:\Users\makmw\AppData\Local\Temp\tmpxfyanui8_test.py:5
=========================== short test summary info ===========================
ERROR ..\..\..\Users\makmw\AppData\Local\Temp\tmpxfyanui8_test.py::test_send_email
============================== 1 error in 0.21s ===============================
1

Part 3: mocker.patch - Automatic Cleanup#

code = '''
import random

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

def test_patch_via_mocker(mocker):
    mocker.patch('random.randint', return_value=4)
    assert roll_die() == 4

def test_unpatched_after(mocker):
    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\tmp83iw2y2p_test.py::test_patch_via_mocker ERROR [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp83iw2y2p_test.py::test_unpatched_after ERROR [100%]

=================================== ERRORS ====================================
___________________ ERROR at setup of test_patch_via_mocker ___________________
file C:\Users\makmw\AppData\Local\Temp\tmp83iw2y2p_test.py, line 7
  def test_patch_via_mocker(mocker):
E       fixture 'mocker' not found
>       available fixtures: _session_faker, anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, dash_br, dash_duo, dash_duo_mp, dash_multi_process_server, dash_process_server, dash_thread_server, dashjl, dashjl_server, dashr, dashr_server, diskcache_manager, doctest_namespace, faker, free_tcp_port, free_tcp_port_factory, free_udp_port, free_udp_port_factory, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, subtests, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

C:\Users\makmw\AppData\Local\Temp\tmp83iw2y2p_test.py:7
___________________ ERROR at setup of test_unpatched_after ____________________
file C:\Users\makmw\AppData\Local\Temp\tmp83iw2y2p_test.py, line 11
  def test_unpatched_after(mocker):
E       fixture 'mocker' not found
>       available fixtures: _session_faker, anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, dash_br, dash_duo, dash_duo_mp, dash_multi_process_server, dash_process_server, dash_thread_server, dashjl, dashjl_server, dashr, dashr_server, diskcache_manager, doctest_namespace, faker, free_tcp_port, free_tcp_port_factory, free_udp_port, free_udp_port_factory, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, subtests, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

C:\Users\makmw\AppData\Local\Temp\tmp83iw2y2p_test.py:11
=========================== short test summary info ===========================
ERROR ..\..\..\Users\makmw\AppData\Local\Temp\tmp83iw2y2p_test.py::test_patch_via_mocker
ERROR ..\..\..\Users\makmw\AppData\Local\Temp\tmp83iw2y2p_test.py::test_unpatched_after
============================== 2 errors in 0.11s ==============================
1

Part 4: pytest-cov - Measuring Coverage#

import tempfile, os, sys
tmpdir = tempfile.mkdtemp()
mymod_path = os.path.join(tmpdir, 'mymod.py')
with open(mymod_path, 'w') as f:
    f.write('def add(a, b):\n    return a + b\n\ndef unused_function():\n    return "never called"\n')
test_path = os.path.join(tmpdir, 'test_mymod.py')
with open(test_path, 'w') as f:
    f.write('from mymod import add\n\ndef test_add():\n    assert add(2, 3) == 5\n')
sys.path.insert(0, tmpdir)
exit_code = pytest.main([test_path, '--cov=mymod', '--cov-report=term-missing', '-v', '--no-header'])
print(exit_code)
4
ERROR: usage: ipykernel_launcher.py [options] [file_or_dir] [file_or_dir] [...]
ipykernel_launcher.py: error: unrecognized arguments: --cov=mymod --cov-report=term-missing
  inifile: None
  rootdir: C:\Users\makmw\AppData\Local\Temp\tmprm1zjysa

Part 5: pytest-xdist - Running Tests in Parallel#

code = '''
def test_a():
    assert 1 + 1 == 2

def test_b():
    assert 2 + 2 == 4

def test_c():
    assert 3 + 3 == 6

def test_d():
    assert 4 + 4 == 8
'''
exit_code = run_pytest(code, ['-n', '2', '-v', '--no-header'])
print(exit_code)
4
ERROR: usage: ipykernel_launcher.py [options] [file_or_dir] [file_or_dir] [...]
ipykernel_launcher.py: error: unrecognized arguments: -n 2
  inifile: None
  rootdir: C:\Users\makmw\AppData\Local\Temp

Part 6: A Real Pattern - mocker in an Ordinary Test Suite#

code = '''
class NotificationService:
    def __init__(self, transport):
        self.transport = transport
        self.sent_count = 0

    def notify(self, user_id, message):
        self.transport.send(user_id, message)
        self.sent_count += 1

def test_notify_calls_transport(mocker):
    fake_transport = mocker.Mock()
    service = NotificationService(fake_transport)
    service.notify(42, 'hello')
    fake_transport.send.assert_called_once_with(42, 'hello')

def test_notify_increments_count(mocker):
    fake_transport = mocker.Mock()
    service = NotificationService(fake_transport)
    service.notify(1, 'a')
    service.notify(2, 'b')
    assert service.sent_count == 2

def test_sent_count_starts_at_zero(mocker):
    service = NotificationService(mocker.Mock())
    assert service.sent_count == 0
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 3 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py::test_notify_calls_transport ERROR [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py::test_notify_increments_count ERROR [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py::test_sent_count_starts_at_zero ERROR [100%]

=================================== ERRORS ====================================
________________ ERROR at setup of test_notify_calls_transport ________________
file C:\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py, line 11
  def test_notify_calls_transport(mocker):
E       fixture 'mocker' not found
>       available fixtures: _session_faker, anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, dash_br, dash_duo, dash_duo_mp, dash_multi_process_server, dash_process_server, dash_thread_server, dashjl, dashjl_server, dashr, dashr_server, diskcache_manager, doctest_namespace, faker, free_tcp_port, free_tcp_port_factory, free_udp_port, free_udp_port_factory, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, subtests, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

C:\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py:11
_______________ ERROR at setup of test_notify_increments_count ________________
file C:\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py, line 17
  def test_notify_increments_count(mocker):
E       fixture 'mocker' not found
>       available fixtures: _session_faker, anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, dash_br, dash_duo, dash_duo_mp, dash_multi_process_server, dash_process_server, dash_thread_server, dashjl, dashjl_server, dashr, dashr_server, diskcache_manager, doctest_namespace, faker, free_tcp_port, free_tcp_port_factory, free_udp_port, free_udp_port_factory, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, subtests, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

C:\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py:17
______________ ERROR at setup of test_sent_count_starts_at_zero _______________
file C:\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py, line 24
  def test_sent_count_starts_at_zero(mocker):
E       fixture 'mocker' not found
>       available fixtures: _session_faker, anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, dash_br, dash_duo, dash_duo_mp, dash_multi_process_server, dash_process_server, dash_thread_server, dashjl, dashjl_server, dashr, dashr_server, diskcache_manager, doctest_namespace, faker, free_tcp_port, free_tcp_port_factory, free_udp_port, free_udp_port_factory, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, subtests, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

C:\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py:24
=========================== short test summary info ===========================
ERROR ..\..\..\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py::test_notify_calls_transport
ERROR ..\..\..\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py::test_notify_increments_count
ERROR ..\..\..\Users\makmw\AppData\Local\Temp\tmpmyh5fkpe_test.py::test_sent_count_starts_at_zero
============================== 3 errors in 0.12s ==============================
1

Wrap-Up: What You Learned#

  • A pytest plugin is a normal, pip-installable package that adds fixtures, markers, or CLI flags.
  • pytest-mock provides the mocker fixture, wrapping unittest.mock with automatic cleanup after each test.
  • mocker.patch works like unittest.mock.patch but needs no with block; it reverts automatically.
  • pytest-cov, via --cov and --cov-report=term-missing, reports coverage percentage and uncovered line numbers.
  • pytest-xdist, via -n, distributes tests across parallel worker processes to speed up a growing suite.
  • Plugin-provided fixtures like mocker mix naturally into an ordinary, otherwise-unmocked test suite.
  • That wraps up plugins. Next up: Coverage With pytest-cov.

Found this useful?

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