Testing with Pytest

Pytest is arguably the most popular and widespread testing framework in the Python community. It is also compatible with Python’s unittest and doctest.

Generate XML report with pytest

To generate XML report, add the --junitxml=<report-path> or --junit-xml=<report-path> option.

Show test function docstring in report

# conftest.py

import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
   outcome = yield
   report = outcome.get_result()

   test_fn = item.obj
   docstring = getattr(test_fn, '__doc__')
   if docstring:
      report.nodeid = docstring


# test_it.py

def test_ok():
   """This is my very important test."""
   print("ok")

This will produce output similar to:

$ pytest -v
test_docstring_in_report.py::test_ok
..\..\..\This is my very important test. PASSED                 [100%]

Pytest built-in fixtures

List of pytest fixtures could be obtained by running pytest -q --fixtures.

Here are some interesting built-in fixtures:

  • capsys

    Enable text capturing of writes to sys.stdout and sys.stderr.

    Returns an instance of CaptureFixture to give access to captured stdout and stderr.

    The captured output is made available via capsys.readouterr() method calls, which return a (out, err) namedtuple. out and err will be text objects.

    Example:

    def test_output(capsys):
       print("hello")
       captured = capsys.readouterr()
       assert captured.out == "hello\n"
    
  • capsysbinary

    Enable bytes capturing of writes to sys.stdout and sys.stderr.

    The captured output is made available via capsysbinary.readouterr() method calls, which return a (out, err) namedtuple. out and err will be bytes objects.

  • capfd

    Enable text capturing of writes to file descriptors 1 and 2.

    The captured output is made available via capfd.readouterr() method calls, which return a (out, err) namedtuple. out and err will be text objects.

  • capfdbinary

    Enable bytes capturing of writes to file descriptors 1 and 2.

    The captured output is made available via capfd.readouterr() method calls, which return a (out, err) namedtuple. out and err will be byte objects.

  • pytestconfig [session scope] Session-scoped fixture that returns the pytest.config.Config object.

    Example:

    def test_foo(pytestconfig):
       if pytestconfig.getoption("verbose") > 0:
            ...
    
  • monkeypatch

    A convenient fixture for monkey-patching.

    The fixture provides these methods to modify objects, dictionaries or os.environ:

    monkeypatch.setattr(obj, name, value, raising=True)
    monkeypatch.delattr(obj, name, raising=True)
    monkeypatch.setitem(mapping, name, value)
    monkeypatch.delitem(obj, name, raising=True)
    monkeypatch.setenv(name, value, prepend=False)
    monkeypatch.delenv(name, raising=True)
    monkeypatch.syspath_prepend(path)
    monkeypatch.chdir(path)
    

    All modifications will be undone after the requesting test function or fixture has finished. The raising parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target.

  • request

    Special fixture of class FixtureRequest providing information of the requesting test function.

  • tmpdir

    Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory.

    The returned object is a py.path.local path object.

  • tmp_path

    Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory.

    The returned object is a pathlib.Path object.

    Note

    In python < 3.6 this is a pathlib2.Path.

Pytest plugins

  • pytest-bdd - Implements a subset of the Gherkin language to enable automating project requirements testing and to facilitate behavioral driven development.

  • pytest-cov - Produces coverage reports.

  • pytest-django - Provides a set of useful tools for testing Django applications and projects.

  • pytest-randomly - Randomly order tests with controlled seed.

  • pytest-reverse - Execute tests in reverse order.

  • pytest-splinter - Provides a set of fixtures to use splinter for browser testing with pytest

  • pytest-xdist - Adds test execution modes, e.g. multi-CPU and distributed.

Running doctest test cases

By default pytest is looking for test_*.txt files and if such a file is found, pytest executes the doctest tests defined in this file.

Pytest can also discover and execute doctest test cases from Python modules. For example if a function has docstring which contains doctest test cases, pytest can execute the tests.

addition_doctest.py
def add(*args):
   """Add one or more numbers and return the result.

   >>> add(3, 2)
   5
   >>> add(5, 4, 3, 2, 3, 4, 5)
   26
   """
   return sum(args)

To execute test cases from modules, specify the --doctest-modules option to pytest.

$ pytest --doctest-modules
============================== test session starts ==============================
platform win32 -- Python 3.8.1, pytest-6.1.0, py-1.9.0, pluggy-0.13.1
rootdir: C:\Sandbox\PoC\python-repl-cmd\src
plugins: cov-2.8.1, django-4.4.0, flask-0.14.0
collected 1 item

addition_doctest.py .                                                      [100%]

=============================== 1 passed in 0.04s ===============================

For further information refer to the pytest doctest integration documentation.

Running unittest test cases

Pytest can discover and execute unittest test cases:

test_addition.py
import unittest

def add(*args):
   return sum(args)

class TestAddition(unittest.TestCase):
   def test_result_is_sum(self):
      result = add(3, 2)
      self.assertEqual(result, 5)

   def test_add_many(self):
      result = add(5, 4, 3, 2, 3, 4, 5)
      self.assertEqual(result, 26)

Running the tests is as easy as:

$ pytest
============================== test session starts ==============================
platform win32 -- Python 3.8.1, pytest-6.1.0, py-1.9.0, pluggy-0.13.1
rootdir: C:\Sandbox\PoC\python-repl-cmd\src
plugins: cov-2.8.1, django-4.4.0, flask-0.14.0
collected 2 items

test_addition.py ..                                                        [100%]

=============================== 2 passed in 0.06s ===============================

This makes it very easy to migrate from unittest to pytest_ or to combine tests that use different frameworks.