Brain Dump

Pytest

Tags
library

Is the standard python testing library.

Note: Pytest is an external testing framework building on the builtin unittest library. Both are described in this file for ease of reference, despite actually being two separate libraries.

Writing Tests

Tests are functions that start with a test_ prefix.

Fixtures

Fixtures are objects or resources that need to be shared across multiple tests in a convenient way. In pytest a fixture is a function that each test-case can accept and whose value can be initialised for the entire test suite, or for each test case.

import pytest


# Note: if you omit name=... the fixture would have the same name
# as the function.
@pytest.fixture(name="container_box")
def container_box_fixture():
    return Box(height=102, width=96, length=240)


def test_container(container_box):
    # pytest automatically passes in container_box with the value of the
    # container_box_fixture() function.
    ...

Note: For convenience you may place fixtures in a conftest.py file. This file is automatically imported by pytest meaning all your test cases can seamlessly use the fixture.

Note: To avoid repeatedly defining a fixtures value when it doesn't change across tests, you can mark the fixture as class scopes, session scoped or module scoped instead of the default function scope.

Cleaning Up Fixture Resources

If your fixture has resources, such as an open file, that needs to be cleaned up after its used you can yield the value of the fixture instead of returning it and then place any cleanup logic afterwards.

@pytest.fixture
def config_file():
    file = open('config.json', 'r')
    yield file
    file.close()

Or better yet, you can use the context-manager pattern to ensure the resource is properly released even if your test throws an exception.

@pytest.fixture
def config_file():
    with open('config.json', 'r') as file:
	yield file

Fixture Dependencies

Any parameters defined by a fixture can be filled in by another fixture. This allows you to write highly modular test code.

import pytest

@pytest.fixture
def config_file():
    return 'config.json'

@pytest.fixture
def config(config_file):
    with open(config_file, 'r') as fd:
	return fd.read()

Marking Tests

The pytest.mark module contains a list of decorators you can apply to test cases to change their behaviour. This includes decorators to skip a test, skipif the test fails some predicate, or to mark a test as xfail meaning you expect the test to fail and it not failing is a failure.

Parameterised Tests

The module also exposes methods to parameterise test cases. This lets you conveniently pass multiple combinations of values without redefining the test logic.

def square(a):
    return a * a

@pytest.mark.parametrize("test_input,expected", [(1, 1), (2, 4), (3, 9), (4, 16)])
def test_square(test_input, expected):
    assert square(test_input) == expected

Note: If you apply parametrize multiple times pytest will use a combination of all the parameterised types.

@pytest.mark.parametrize("foo", [1, 2])
@pytest.mark.parametrize("foo", [3, 4])
def test_foo(foo, bar):
    # Will output:
    # 1, 3
    # 1, 4
    # 2, 3
    # 2, 4
    print(foo, bar)

Custom Marks

Trying to use a mark function that hasn't been defined will cause pytest to generate one for you. This allows you to apply custom markers to your own tests and later use these to, for example, only run tests with a given mark.

@pytest.mark.foobar
def test_foo():
    ...

Mocking

Is the act of generating objects whose members and return values are fixed and that track how the test may interact with those members during a tests execution.

In python mocks are provided by the unittest library. A unittest.Mock object is an object that can mock another object.

from unittest import mock

m1 = mock.Mock(an_attribute="foo")
m1.an_attribute  # => 'foo'

# If you attempt to access an undefined attribute then the mock will
# implicitly generate a new attribute and initialise it as a new mock
m1.something_else  # => unittest.Mock(...)
# To prevent this from automatically happening you must seal the mock
unittest.mock.seal(m1)
# m1.something_new  # => AttributeError

# Passing a return_value to a Mock makes it a function mock, that
# returns the given value when called.
m2 = mock.Mock(return_value="bar")
m2()  # => 'bar'
m2()  # => 'bar'
m2("foo")  # => 'bar'
# Function mocks keep track of how many times they've been called and
# any arguments that were passed to them.
m2.call_count  # => 3
m2.call_list  # => [call(), call(), call('foo')]

# They also have several built in assertion functions for basic use cases.
m2.assert_called_once()

# You can attach side affects to mocks.
mock.Mock(side_effect=[1, 2, 3])  # Will return one value at a time when called.
mock.Mock(side_effect=TypeError)  # Will raise an exception when called
mock.Mock(side_effect=lambda x: x * 2)  # Will run side effect function when called

# Mocks don't implement magic methods such as __len__ by default. To use
# these use a MagicMock.
len(mock.MagicMock())  # => 0
# With a type argument to the MagicMock only magic methods defined by that type
# are implemented in the MagicMock.
-mock.MagicMock(int)

Patching Libraries with Mocks

You can automatically patch out calls to an external module using unittest.mock.patch.

import os
from unittest import mock
from unittest.mock import patch


def test_makesdir():
    with patch.object(os.path, "mkdir", autospec=True) as mkdir_mock:
	...  # Run test which eventually calls os.path.mkdir()
	mkdir_mock.assert_called_once()

Warn: This only works if at the caller site the caller access the method through the module, not by importing or accessing it directly (example: from os.path import mkdir; mkdir('foo')).

Note: This logic may be better served in a fixture instead of a test-case.

Note: patch.object for simple use cases can be used as a decorator.