Skip to the content.

PyTest - Testing Framework

Installation

pipx install pytest

or:

pipenv install --dev pytest

Basic Usage

import pytest

def test_module():
    assert True

class TestClass:

    def test_equal(self):
        assert 'Hello '.capitalize() == 'Hello '

    def test_true(self):
        assert 'HELLO'.isupper()
        assert not 'Hello'.islower()

    def test_in(self):
        assert 'a' in 'abc'
        assert 'd' not in 'abc'

    def test_raise(self):
        with pytest.raises(TypeError):
            'hello'.capitalize(0)

Run

All Test Cases

pytest [-q|--quiet]

Specified Modules, Functions, Methods

pytest <xxx.py>
pytest <xxx.py>::<func_name>
pytest <xxx.py>::<ClassName>::<method_name>

Filter

pytest -k <containing-expr e.g. "MyClass and not method">

pytest -m <mathing-expr e.g. "MyClass and not method">

Profiling

show M slowest (greater than N seconds) setup/test durations:

pytest --durations=<M> --durations-min=<N>

Plugin

pytest -p <plugin-name>

disable:

pytest -p no:<plugin-name>

Test Fixture

import pytest

@pytest.fixture
def setup():
    return 1

def test_module(setup)
    assert setup == 1

@pytest.fixture(autouse=True)
def setup_for_all():
    return 2

Scope

Fixture scopes

# scope: function (default), class, module, package, session
@pytest.fixture(scope='module')
def setup_for_module():
    return 2

Dynamic scope

def determine_scope(fixture_name, config):
    if config.getoption("--keep-containers", None):
        return "session"
    return "function"

@pytest.fixture(scope=determine_scope)
def setup():
    return 2

Safe Teardown

@pytest.fixture
def setup():
    yield 1
    # code to tear down (clean up).

Pass Data

import pytest

@pytest.fixture
def xxx(request):
    marker = request.node.get_closest_marker("xxx_data")
    if marker is None:
        # Handle missing marker in some way...
        data = None
    else:
        data = marker.args[0]

    # Do something with the data
    return data + 1


@pytest.mark.xxx_data(1)
def test_xxx(xxx):
    assert xxx == 2

Parametrizing

import pytest

@pytest.fixture(params=['127.0.0.1', '192.168.0.1'], autouse=True)
def xxx(request):
    connection = conn(request.param)
    yield connection
    connection.close()


@pytest.fixture
def yyy():
    return 1

@pytest.mark.parametrize('yyy', ['directly-overridden-yyy'])
def test_username(yyy):
    assert yyy == 'directly-overridden-yyy'

Capture Log

import logging

import pytest

class TestClass:

    def test_log(self, caplog):
        # caplog.set_level(logging.INFO)
        # caplog.set_level(logging.INFO, logger='root.xxx')
        with caplog.at_level(logging.INFO, logger='root.xxx'):
            logging.getLogger('root.xxx').info('first message')
            logging.getLogger('root.xxx.yyy').error('second message')

        assert len(caplog.records) == 2
        assert caplog.records[0].message == 'first message'
        assert caplog.records[1].message == 'second message'
        assert caplog.records[0].levelno == logging.INFO

Capture stdout/stderr

import sys

import pytest

def test_output(capsys):  # or use "capfd" for fd-level
    print('hello')
    sys.stderr.write('world\n')

    captured = capsys.readouterr()
    assert captured.out == 'hello\n'
    assert captured.err == 'world\n'

    print('next')
    captured = capsys.readouterr()
    assert captured.out == 'next\n'

    with capsys.disabled():
        print("output not captured, going directly to sys.stdout")

Run:

pytest --capture=sys  # fd

Capture Warnings

import warnings

import pytest

class TestClass:

    def test_warning(self):
        with pytest.warns(UserWarning):
            warnings.warn('xxx', UserWarning)

    def test_warning_match(self):
        with pytest.warns(UserWarning, match='must be 0 or None'):
            warnings.warn('value must be 0 or None', UserWarning)

        with pytest.warns(UserWarning, match=r'must be \d+$'):
            warnings.warn('value must be 42', UserWarning)

    def test_warning_record(self):
        with pytest.warns() as record:
            warnings.warn('user', UserWarning)
            warnings.warn('runtime', RuntimeWarning)

        assert len(record) == 2
        assert str(record[0].message) == 'user'
        assert str(record[1].message) == 'runtime'

    def test_warning_recwarn(self, recwarn):
        warnings.warn('hello', UserWarning)
        assert len(recwarn) == 1
        w = recwarn.pop(UserWarning)
        assert issubclass(w.category, UserWarning)
        assert str(w.message) == 'hello'
        assert w.filename
        assert w.lineno

Skip Tests

import sys

import pytest

class TestClass:

    @pytest.mark.skip(reason='demonstrating skipping')
    def test_skip(self):
        pytest.xfail('shouldn\'t happen')

    @pytest.mark.skipif(sys.version_info < (3, 9), 'python 3.9+ required')
    def test_skipif(self):
        # Tests that work for only a certain version of Python.
        pass

    def test_maybe_skipped(self):
        if not external_resource_available():
            pytest.skip('external resource not available')
        # test code that depends on the external resource
        pass

Expected Failure

import pytest

class TestExpectedFailure:

    @pytest.mark.xfail
    def test_fail(self):
        assert False

Temp files

import pytest

def test_tmpfiles(self, tmp_path):
    print(tmp_path)
    assert True

References