Common Patterns for Tests

In this section you will learn some useful features of pytest that can make your tests succinct and easy to maintain.

Parametrized Tests

Tests that apply the same general test logic to a collection of different parameters can use parametrized tests. For example, this:

import numpy as np
from ..refraction import snell


def test_perpendicular():
    # For any indexes, a ray normal to the surface should not bend.
    # We'll try a couple different combinations of indexes....

    actual = snell(0, 2.00, 3.00)
    expected = 0
    assert actual == expected

    actual = snell(0, 3.00, 2.00)
    expected = 0
    assert actual == expected

can be rewritten as:

import numpy as np
import pytest
from ..refraction import snell


@pytest.mark.parametrize('n1, n2',
                         [(2.00, 3.00),
                          (3.00, 2.00),
                         ])
def test_perpendicular(n1, n2):
    # For any indexes, a ray normal to the surface should not bend.
    # We'll try a couple different combinations of indexes....

    actual = snell(0, n1, n2)
    expected = 0
    assert actual == expected

The string 'n1, n2' specifies which parameters this decorator will fill in. Pytest will run test_perpendicular twice, one for each entry in the list [(2.00, 3.00), (3.00, 2.00)], passing in the respective values n1 and n2 as arguments.

From here we refer you to the pytest parametrize documentation.

Fixtures

Tests that have different logic but share the same setup code can use pytest fixtures. For example, this:

import numpy as np


def test_height():
    # Construct a 1-dimensional Gaussian peak.
    x = np.linspace(-10, 10, num=21)
    sigma = 3.0
    peak = np.exp(-(x / sigma)**2 / 2) / (sigma * np.sqrt(2 * np.pi))
    expected = 1 / (sigma * np.sqrt(2 * np.pi))
    # Test that the peak height is correct.
    actual = np.max(peak)
    assert np.allclose(actual, expected)


def test_nonnegative():
    # Construct a 1-dimensional Gaussian peak.
    x = np.linspace(-10, 10, num=20)
    sigma = 3.0
    peak = np.exp(-(x / sigma)**2 / 2) / (sigma * np.sqrt(2 * np.pi))
    # Test that there are no negative values.
    assert np.all(peak >= 0)

can be written as:

import pytest
import numpy as np


@pytest.fixture
def peak():
    # Construct a 1-dimensional Gaussian peak.
    x = np.linspace(-10, 10, num=21)
    sigma = 3.0
    peak = np.exp(-(x / sigma)**2 / 2) / (sigma * np.sqrt(2 * np.pi))
    return peak


def test_height(peak):
    expected = 1 / (sigma * np.sqrt(2 * np.pi))
    # Test that the peak height is correct.
    actual = np.max(peak)
    assert np.allclose(actual, expected)


def test_nonnegative(peak):
    # Test that there are no negative values.
    assert np.all(peak >= 0)

To reuse a fixture in multiple files, add it to conftest.py located in the tests/ directory. It will automatically be imported by pytest into each test module.

From here we refer you to the pytest fixtures documentation.

Skipping Tests

Sometimes it is useful to skip specific tests under certain conditions. Examples:

import pytest
import sys


@pytest.mark.skipif(sys.version_info < (3, 7),
                    reason="requires python3.7 or higher")
def test_something():
    ...


@pytest.mark.skipif(sys.platform == 'win32',
                    reason="does not run on windows")
def test_something_that_does_not_work_on_windows():
    ...


def test_something_that_needs_a_special_dependency():
    some_library = pytest.importorskip("some_library")
    ...

From here we refer you to the pytest skipping documentation.