pytest

Table of Contents

Overview

Commandline Usage

pytest --version   # shows where pytest was imported from
pytest --fixtures  # show available builtin function arguments
pytest -h | --help # show help on command line and config file options
pytest -x          # stop after first failure
pytest --maxfail=2 # stop after two failures
pytest test_mod.py # run tests in module
pytest somepath    # run all tests below somepath

pytest --capture=fd  # OS level; 1,2 fds are captured
pytest --capture=sys # sys module level; sys.stdout, sys.stderr are captured
pytest --capture=no  # nothing captured
pytest -s            # same as --capture=no
# only run tests with names that match the # "string expression".
# e.g. "MyClass and not method" # will select
# TestMyClass.test_something but not TestMyClass.test_method_simple
pytest -k stringexpr

# only run tests that match the "node ID",
pytest test_mod.py::test_func
pytest test_mod.py::TestClass::test_method

pytest --pyargs pkg # run all tests found below directory of pkg
pytest --showlocals    # show local variables in tracebacks
pytest -l              # show local variables (shortcut)

pytest --tb=auto       # (default) 'long' tracebacks for the first and last
                       # entry, but 'short' style for the other entries
pytest --tb=long       # exhaustive, informative traceback formatting
pytest --tb=short      # shorter traceback format
pytest --tb=line       # only one line per failure
pytest --tb=native     # Python standard library formatting
pytest --tb=no         # no traceback at all

pytest --pdb           # drop into the PDB prompt
pytest --durations=10  # To get a list of the slowest 10 test durations

Reference

approx

>>> from pytest import approx
>>> 0.1 + 0.2 == approx(0.3)
True
>>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
True

fixture

scope
  • session
  • module
  • function (default)
# content of ./test_smtpsimple.py
import pytest

@pytest.fixture
def smtp():
    import smtplib
    return smtplib.SMTP("smtp.gmail.com")

def test_ehlo(smtp):
    response, msg = smtp.ehlo()
    assert response == 250
    assert 0 # for demo purposes

There are several ways to use a fixture:

# Specify it as a function argument
@pytest.fixture
def foo():
    pass

def test_bar(foo):
    pass

# Only wants the fixture's effects
@pytest.mark.usefixtures('foo')
def test_baz():
    pass

# autouse=True for 'module' or 'session' scope
@pytest.fixture(scope='session', autouse=True)
def foo():
    pass
@pytest.fixture(scope="module")  # only be invoked once per test module
def smtp():
    smtp = smtplib.SMTP("smtp.gmail.com")
    yield smtp  # provide the fixture value
    print("teardown smtp")
    smtp.close()


# request example
@pytest.fixture(scope="module")
def smtp(request):
    server = getattr(request.module, "smtpserver", "smtp.gmail.com")
    smtp = smtplib.SMTP(server)
    yield smtp
    print ("finalizing %s (%s)" % (smtp, server))
    smtp.close()


# Each test case will be executed for
# each fixture based on each element of params
@pytest.fixture(scope="module",
                params=["smtp.gmail.com", "mail.python.org"])
def smtp(request):
    smtp = smtplib.SMTP(request.param)
    yield smtp
    print ("finalizing %s" % smtp)
    smtp.close()


# Set ids to inhence the readability of tests
@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
def a(request):
    return request.param

def test_a(a):
    pass

# pytest prints out like:
#   <Function 'test_a[spam]'>
#   <Function 'test_a[ham]'>

pytest minimizes the number of active fixtures during test runs. If you have a parametrized fixture, then all the tests using it will first execute with one instance and then finalizers are called before the next fixture instance is created.

For example:

  1. You define a fixture named a
  2. You define fixtures of b and c depending on a
  3. You define a test test_x uses fixtures of b and c

In this case, the fixture a is called only once for each run of test_x

@pytest.fixture
def fruit():
    pass

@pytest.fixture
def apple(fruit):
    pass

@pytest.fixture
def banana(fruit):
    pass

def test_foo(apple, banana):
    # For this test, 'fruit' is executed once.
    # 'apple' and 'banana' uses the same 'fruit'
    pass

mark

import pytest
@pytest.mark.parametrize("test_input,expected", [
    ("3+5", 8),
    ("2+4", 6),
    ("6*9", 42),
])
def test_eval(test_input, expected):
    assert eval(test_input) == expected

monkeypatch

import module
def test_monkeypatch(monkeypatch):
    monkeypatch.setattr(module, 'name', 'value')
    monkeypatch.setattr('module.name', 'value')  # same as above
    monkeypatch.delattr('module.name')

    d = {}
    monkeypatch.setitem(d, 'key', 'value')
    monkeypatch.delitem(d, 'key')

    # environment variable
    monkeypatch.setenv('FOO', 'VALUE')
    monkeypatch.delenv('FOO')

    monkeypatch.syspath_prepend('./bin')
    monkeypatch.chdir('../')

    monkeypatch.undo()  # undo all changes

raises

import pytest


def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0


def test_recursion_depth():
    with pytest.raises(RuntimeError) as exc_info:
        def f():
            f()
        f()
    assert exc_info.match('maximum recursion' )

fail

def test_foo3():
    try:
        foo(7)
    except MyError:
        pytest.fail("Unexpected MyError ..")

Topics

rootdir

The rootdir is used a reference directory for constructing test addresses (“nodeids”) and can be used also by plugins for storing per-testrun information.

pytest.config.rootdir
pytest.config.inifile

Import Mechanisms

Test Layouts


Useful if you have many functional tests or for other reasons want to keep tests separate from actual application code (often a good idea):

setup.py   # your setuptools Python package metadata
mypkg/
    __init__.py
    appmodule.py
tests/
    test_app.py
    ...

Useful if you have direct relation between (unit-)test and application modules and want to distribute your tests along with your application:

setup.py   # your setuptools Python package metadata
mypkg/
    __init__.py
    appmodule.py
    ...
    test/
        test_app.py
        ...

pytest tests/test_app.py       # for external test dirs
pytest mypkg/test/test_app.py  # for inlined test dirs
pytest mypkg                   # run tests in all below test directories
pytest                         # run all tests below current dir

Plugins

pytest-watch

pip install pytest-watch
ptw --runner "pytest -s"
ptw --onpass "say passed" --onfail "say failed"

Links