Roman Imankulov

Roman Imankulov

Full-stack Python web developer from Porto

search results (esc to close)
07 Mar 2023

Parametrizing Pytest Fixtures

Parametrizing Pytest Fixtures
Photo by Markus Winkler

Parametrizing pytest fixtures is a powerful technique for writing more concise and readable tests. I find parametrizing fixtures helpful in two cases.

  • When testing multiple implementations of the same interface.
  • When testing the same function against different inputs.

Testing interface implementations

Thanks to Python duck typing, you don’t have to explicitly define interfaces. Any object that has the required attributes and methods can be used as an interface implementation.

For the sake of exercise, let’s test python modules json and pickle. Each has “dumps” and “loads” methods, providing an implicit interface for serializing and restoring data. Below, we employ a parametrized fixture and write tests to ensure serializing and deserializing work as expected.

# file: test_serializers.py
import json
import pickle

import pytest


@pytest.fixture(params=[json, pickle])
def serializer(request):
    return request.param


def test_serializer(serializer):
    obj = {'a': 1, 'b': 2}
    serialized = serializer.dumps(obj)
    deserialized = serializer.loads(serialized)
    assert obj == deserialized

Looking at the output, you can notice that the test has been executed twice: with “json” and “pickle” passed as a “serializer” parameter.

$ pytest -vv test_serializers.py

test_serializers.py::test_serializer[json] PASSED
test_serializers.py::test_serializer[pickle] PASSED

Parametrized fixtures are not that helpful for a single test — instead, you can parametrize the test function directly with pytest.mark.parametrize. However, they become more useful when you use the same fixture in multiple tests or perform extra steps to initialize fixture objects.

Testing different input

Here is a slightly different scenario. Imagine you test Pillow, a Python imaging library, to ensure it can resize all supported file formats. Unlike the above, the example below does not test various “actors.” Instead, the same actor is tested against different inputs.

from PIL import Image


@pytest.fixture(params=["./image.jpg", "./image.png"])
def image_path(request):
    return request.param


def test_resize_image(image_path):
    # resize the image
    image = Image.open(image_path)
    image = image.resize((100, 100))

    # save it
    dest_path = f"resized_{image_path}"
    image.save(dest_path)

    # check the dimensions of the resized file
    assert Image.open(dest_path).size == (100, 100)

Combining parametrized fixtures

When using multiple parametrized fixtures, pytest will run the test for each parameter combination. The example below tests two serializers against two different inputs.

# file: test_serializers.py
import json
import pickle

import pytest


@pytest.fixture(params=[json, pickle])
def serializer(request):
    return request.param


@pytest.fixture(params=[1, "foo"])
def obj(request):
    return request.param


def test_serializer(serializer, obj):
    serialized = serializer.dumps(obj)
    deserialized = serializer.loads(serialized)
    assert obj == deserialized

The output shows that the test has been run four times:

test_serializers.py::test_serializer[json-1] PASSED
test_serializers.py::test_serializer[json-foo] PASSED
test_serializers.py::test_serializer[pickle-1] PASSED
test_serializers.py::test_serializer[pickle-foo] PASSED

Skipping tests that shouldn’t work

If you are heavy on parameterized fixtures, sooner or later, you come across a case that doesn’t work for one combination of parameters. Pytest has a special skip() directive for this case.

Let’s extend the previous example with a datetime object, remembering that datetime is not JSON-serializable.

# file: test_serializers.py
import json
import pickle
import datetime

import pytest


@pytest.fixture(params=[json, pickle])
def serializer(request):
    return request.param


@pytest.fixture(params=[1, "foo", datetime.datetime.now()])
def obj(request):
    return request.param


def test_serializer(serializer, obj):
    if isinstance(obj, datetime.datetime) and serializer is json:
        pytest.skip("datetime objects are JSON-serialized")
    serialized = serializer.dumps(obj)
    deserialized = serializer.loads(serialized)
    assert obj == deserialized

Here’s the output.

...
test_serializers.py::test_serializer[json-1] PASSED
test_serializers.py::test_serializer[json-foo] PASSED
test_serializers.py::test_serializer[json-obj2] SKIPPED (datetime objects are JSON-serialized)
... (more lines below) ...

As you can see, our specific case was skipped with a readable context message.

Roman Imankulov

Hey, I am Roman, and you can hire me.

I am a full-stack Python web developer who loves helping startups and small teams turn their ideas into products.

More about me and my skills