Python pytest

From wikinotes

Pytest produces more readable test failures, reduces boilerplate in tests, has a flexible builtin test-runner, and supports integration of pdb on failed tests.


Documentation

official docs https://docs.pytest.org/en/latest/
config format docs https://docs.pytest.org/en/latest/customize.html
config option docs https://docs.pytest.org/en/latest/reference.html#configuration-options
official api reference https://docs.pytest.org/en/latest/reference.html

Locations

{project}/pytest.ini
{project}/pyproject.toml
{project}/setup.cfg
pytest config locations (pytest recommends pyproject.toml)
{project}/tests/test_*/test_*.py default test structure
{project}/tests/conftest.py fixtures/helper functions available to tests in this directory, and those below it.

Configuration

configuration is performed in one of pyproject.toml, pytest.ini, setup.cfg, tox.ini.

pyproject.toml

[tool.pytest.ini_options]
markers = ["focus: selector for tests under development"]
python_files = "*.py"
testpaths = ["tests/unit", "tests/integration"]

setup.cfg

# setup.cfg
[aliases]
# ``python setup.py test`` will run pytest
# (requires 'pytest-runner')
test=pytest

[tool:pytest]
python_files = *.py
testpaths = tests

norecursedirs = tests/integration/resources

Usage

pytest                                         # run all tests
pytest path/to/test.py                         # run tests within dir
pytest path/to/dir                             # run tests within this dir and subdirs
pytest -k "MyClass and not method"             # runs tests containing string 'MyClass' and not tests containing string 'method'
pytest path/to/test.py::test_func              # run specific test
pytest path/to/test.py::Test_Class::test_func  # run specific class-method test
pytest -k "MyClass::method[paramA-paramB]"     # run a specific parametrized test
pytest -m slow                                 # run tests marked with decorator `@pytest.mark.slow`

Framework Usage

general

import pytest

class Test_MyClass:
    def test_do_something(self):
        assert do_something() == 'success'

expect exceptions

import pytest

class Test_MyClass:
    def test_expectfail(self):
        with pytest.raises(RuntimeError, message='Expecting RuntimeError when ...'):
            do_something()

parametrize tests

import pytest

@pytest.mark.parametrize( 'a, b, result', [
    (1,1,2),
    (2,1,3),
    (10,0,10),
])
def test_sum(a, b, result):
    assert (a+b) == result

fixtures (setup/teardown alternative)

fixtures are pre-built functions that you can use as parameters to your tests. Less code to read, less code to write.

Note that you can share fixtures with all of your tests, by adding them to the file tests/conftest.py.

@pytest.fixture
def myinstance():
    instance = myclass.MyInstance()
    instance._attr = 'test value'
    instance._method = mock.Mock()
    return instance

def test_myinstance(myinstance):
    assert myinstance.do_something() == 'success'

conftest.py

You can load test helper libraries from conftest to expose functions.

# test/conftest.py

import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), 'helpers'))
# test/helpers/database_helpers.py

def mock_database_connection():
    db = mock.MagicMock()
    cursor = mock.MagicMock()
    db.cursor = cursor
    return db
# test/unit/test_something.py

import database_helpers

class TestSomething:
    def setup(self):
        self.db = database_helpers.mock_database_connection()

    def test_foo(self):
        # ...

You can also define fixtures.

setup/teardown

class Test_YourClass(object):

    # ================================
    # once-per-testclass instantiation
    # ================================
    @classmethod 
    def setup_class(cls):
        pass

    @classmethod 
    def teardown_class(cls):
        pass

    # ====================
    # once-per-method call
    # ====================
    def setup_method(self, method):
        pass

    def teardown_method(self, method):
        pass

skipping tests

class TestFoo:
    def test_foo(self):
        pytest.skip('flaky test, needs fixing')

    @pytest.mark.skip(reason='flaky test, needs fixing')
    def test_bar(self):
        # test contents

    @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher")
    def test_baz(self):
        # test contents

focusing specific tests

# ${PROJECT}/pytest.ini
[pytest]
markers =
    focus: being developed now
class TestSomething:
    @pytest.mark.focus
    def test_something(self):
        assert 1 == 2
pytest -m focus   # run all focus tests
pytest --markers  # list all markers

Extensions

python pytest-cov check code-coverage