Skip to main content

unittest-fixtures: A small library for loading unittest fixtures

Description

unittest-fixtures spun off from my Gentoo Build Publisher project. I use unittest, the test framework in the Python standard library, where it's customary to define a TestCase's fixtures in the .setUp() method. Having done it this way for years, it occurred to me one day that this goes against OCP. What if instead of cracking open the .setUp() method to add a fixture to a TestCase one could instead add a decorator? That's what unittest-fixtures allows one to do.

from unittest_fixtures import given

@given(dog)
class MyTest(TestCase):
    def test_method(self, fixtures):
        dog = fixtures.dog

In the above example, dog is a fixture function. Fixture functions are passed to the given decorator. When the test method is run, the fixtures are "instantiated" and attached to the fixtures keyword argument of the test method.

Fixture functions

Fixture functions are functions that one defines that return a "fixture". For example the above dog fixture might look like this:

from unittest_fixtures import fixture

@fixture()
def dog(fixtures):
    return Dog(name="Fido")

Fixture functions are always passed a Fixtures argument. Because fixtures can depend on other fixtures. For example:

@fixture(dog)
def person(fixtures):
    p = Person(name="Jane")
    p.pet = fixtures.dog
    return p

Fixture functions can have keyword parameters, but those parameters must have defaults.

@fixture
def dog(fixtures, name="Fido"):
    return Dog(name=name)

Then one's TestCase can use the where decorator to passed the parameter:

from unittest_fixtures import given, where

@given(dog)
@where(dog__name="Buddy")
class MyTest(TestCase):
    def test_method(self, fixtures):
        dog = fixtures.dog
        self.assertEqual("Buddy", dog.name)

Duplicating fixtures

The unittest-fixtures library allows one to use a fixture more than once. This can be done by passing the fixture as a keyword argument giving different names to the same fixture. Different parameters can be passed to them:

@given(fido=dog, buddy=dog)
@where(fido__name="Fido", buddy__name="Buddy")
class MyTest(TestCase):
    def test_method(self, fixtures):
        self.assertEqual("Buddy", fixtures.buddy.name)
        self.assertEqual("Fido", fixtures.fido.name)

Fixture-depending fixtures will all use the same fixture, but only if they have the same name. So in the above example, if we also gave the TestCase the person fixture, that person would have a different dog because it depends on a fixture called "dog". However this will work:

@given(dog, person)
class MyTest(TestCase):
    def test_method(self, fixtures):
        dog = fixtures.dog
        person = fixtures.person
        self.assertIs(person.pet, dog)

@where (fixture parameters)

The where decorator can be used to pass parameters to a fixture function. Fixture functions are not required to take arguments. To pass a parameter to a function, for example pass name to the dog fixture it's the name of the function, followed by __ followed by the parameter name. For example: dog__name. Fixture functions can also have a parameter that is the same name as the fixture itself. For example:

@given(settings)
@where(settings={"DEBUG": True, "SECRET": "sauce"})
class MyTest(TestCase):
    ...

There are times when one may desire to pass a fixture parameter that uses the value of another fixture, however that value does not get calculated until each test is run. The Param type allows one to accomplish this:

from unittest_fixtures import Param, given, where


@given(person)
@where(person__name=Param(lambda fixtures: fixtures.name))
@given(name=random_choice)
@where(name__choices=["Liam", "Noah", "Jack", "Oliver"])
class MyTest(TestCase):
    ...

Fixtures as context managers

Sometimes a fixture will need a setup and teardown process. If unittest-fixtures is supposed to remove the need to open setUp(), then it must also remove the need to open tearDown(). And it does this by by defining itself as a generator function. For example:

import tempfile

@fixture()
def tmpdir(fixtures):
    with tempfile.TemporaryDirectory() as tempdir:
        yield tempdir

Using the unittest.mock library is another good example of using context manager fixtures.

fixture-depending fixtures

As stated above, fixtures can depend on other fitures. This is done by "declaring" the dependencies in the fixture decorator. Fixtures are then passed as an argument to the fixture function:

@fixture(settings, tmpdir)
def jenkins(fixtures, root=None):
    root = root or fixtures.tmpdir
    settings = replace(fixtures.settings, STORAGE_PATH=root)
    return Jenkins.from_settings(settings)

The above example shows that one can get pretty fancy... or creative with one's fixture definitions.

Fixtures can also have named dependencies. So in the above example, if one wanted a different tmpdir than the "global" one:

@fixture(settings, jenkins_root=tmpdir)
def jenkins(fixtures, root=None):
    root = root or fixtures.jenkins_root
    settings = replace(fixtures.settings, STORAGE_PATH=root)
    return Jenkins.from_settings(settings)

If a TestCase used both jenkins and tmpdir:

@given(tmpdir, jenkins)
class MyTest(TestCase):
   def test_something(self, fixtures):
       self.assertNotEqual(fixtures.jenkins.root, fixtures.tmpdir)

Again if the two fixtures have different names then they are two separate fixtures. In general one should not use named fixtures unless one wants multiple fixtures of the same type.

@params (parametrized tests)

Not to be confused with @parametrized (below) which works similarly. The params decorator turns a TestCase's methods into parametrized tests, however, unlike @parametrized, the parameters are passed into the fixtures argument instead of additional arguments to the test method. For example:

from unittest_fixtures import params

@params(number=[1, 2, 3], square=[1, 4, 9])
class MyTest(TestCase):
    def test(self, fixtures):
        self.assertEqual(fixtures.number**2, fixtures.square)

In the above example, the test method is called three times. With each iteration the fixtures parameter has the values:

  1. Fixtures(number=1, square=1)
  2. Fixtures(number=2, square=4)
  3. Fixtures(number=3, square=9)

Using this method, and Param above, a parameterized tests can use a test parameter as a parameter in a fixture. Consider the example:

@given(dog)
@params(name=["Fido", "Spot", "Rex"])
@where(dog__name=Param(lambda fixtures: f"{fixtures.name} Woofington"))
class MyTest(TestCase):
    def test(self, fixtures):
        print(fixtures.dog.name)

When run this test produces the output:

Fido Woofington
Spot Woofington
Rex Woofington

@parametrized

The @parametrized decorator that acts as a wrapper for unittest's subtests. Unlike @params above, this decorator is to be applied to TestCase methods rather than tests themselves. In this case extra parameters are passed to the test method. This can be used if you only want to parameterize a specific test method in a TestCase rather than all test methods.

For example:

from unittest_fixtures import parametrized

class ParametrizeTests(TestCase):
    @parametrized([[1, 1], [2, 4], [3, 9]])
    def test(self, number, square):
        self.assertEqual(number**2, square)

@combine (parametrized tests using the product of values)

The combine decorator turns a TestCase's methods into parametrized tests similar to @params, however, the parameters are combined as taking a cartesian product of the parameters. For example:

@combine(first=["Spuds", "Rin", "Old"], last=["MacKenzie", "Tin Tin", "Yeller"])
class MyTest(TestCase):
    def test(self, fixtures):
        print(f"{fixtures.first} {fixtures.last}")

When run this test produces the output:

Spuds MacKenzie
Spuds Tin Tin
Spuds Yeller
Rin MacKenzie
Rin Tin Tin
Rin Yeller
Old MacKenzie
Old Tin Tin
Old Yeller

The @combine decorator can also be used in combination with @params and, of course, @given fixtures.

The fixtures kwarg may be overridden

The fixtures keyword argument is automatically passed to TestCase methods when the test is run. The name of the keyword argument can be overridden as follows:

@given(dog)
class MyTest(TestCase):
    unittest_fixtures_kwarg = "fx"

    def test_method(self, fx):
        dog = fx.dog

Recipes

The following are real-world examples of using unittest-fixtures.

Random Number Generator

From larry:

import random as stdlib_random
from unittest import mock

from unittest-fixtures import fixture


@fixture()
def random(fixtures, target="larry.color.random", seed=1):
    _random = stdlib_random.Random(seed)
    with mock.patch(target, new=_random):
        yield _random

Temporary Directory

Also from larry:

import tempfile

from unittest-fixtures import fixture


@fixture()
def tmpdir(fixtures):
    with tempfile.TemporaryDirectory() as tempdir:
        yield tempdir

Mock Environment Variables

From gbp-webhook-playsound:

import os


@fixture()
def environ(fixtures, environ=None, clear=True):
    with mock.patch.dict(os.environ, clear=clear):
        os.environ.update(environ or {})
        yield os.environ

Local Timezone

From gbpcli:

import datetime as dt
from unittest import mock


LOCAL_TIMEZONE = dt.timezone(dt.timedelta(days=-1, seconds=61200), "PDT")


@fixture()
def local_timezone(fixtures, local_timezone=LOCAL_TIMEZONE):
    with mock.patch("gbpcli.render.LOCAL_TIMEZONE", new=local_timezone):
        yield local_timezone

Django HttpRequest

From gbp-feeds:

from django.http import HttpRequest


@fixture()
def request(fixtures, path="/feed.atom", server_name="testserver", server_port=80):
    request = HttpRequest()
    request.path = path
    request.META["SERVER_NAME"] = server_name
    request.META["SERVER_PORT"] = server_port

    return request

sys.argv

From gbp-webhook:

import mock
import sys


@fixture()
def argv(fixtures, argv=None):
    argv = ["gbp", "webhook", "serve"] if argv is None else list(argv)

    with mock.patch.object(sys, "argv", new=argv):
        yield argv

mock.patch

Instead of applying mock.patch and/or mock.patch.object decorators to a TestCase, one can instead use a fixture. This way mocked objects appear as part of the fixtures test parameter instead of having separate arguments for mocks. The following recipe demonstrates how this can be done.

From gbp-testkit (a component of gentoo-build-publisher):

NO_OBJECT = object()  # sentinel value

@fixture()
def patch(_, target="", object=NO_OBJECT, **kwargs):
    if not target:
        patcher = None
        fake = mock.Mock(**kwargs)
    elif object is _NO_OBJECT:
        patcher = mock.patch(target, **kwargs)
        fake = patcher.start()
    else:
        patcher = mock.patch.object(object, target, **kwargs)
        fake = patcher.start()

    yield fake

    if patcher:
        patcher.stop()

So with this the above sys.argv fixture can be replaced with:

@given(argv=patch)
@where(argv__object=sys, argv__target="argv", argv__new=["gbp", "webhook", "serve"])
class MyTest(TestCase)
    def test_method(self, fixtures):
         status_code = main()

         self.assertEqual(status_code, 0)

Alternatively, insteady of passing the sys object one could also:

@where(argv__target="sys.argv", argv__new=["gbp", "webhook", "serve"])

Metadata

Release files for unittest-fixtures 2.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for unittest-fixtures 2.3.0
File Size Uploaded Provenance Actions
unittest_fixtures-2.3.0.tar.gz 17.5 kB Without provenance Details for unittest_fixtures-2.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for unittest-fixtures 2.3.0
File Interpreter ABI Platform Actions
unittest_fixtures-2.3.0-py3-none-any.whl Python 3 none any Details for unittest_fixtures-2.3.0-py3-none-any.whl

Total release size: 28.0 kB

Release files / unittest_fixtures-2.3.0.tar.gz

Download URL unittest_fixtures-2.3.0.tar.gz
Size 17.5 kB
Tags Source
SHA-256 checksum
How to use checksums
aab82eb8949505340f24eb00e8be0a3f406643d5092257324302cc74a6a16898
BLAKE2b-256 checksum
How to use checksums
c41bff45c247afe70b90dc612945505175c85581ed4f8924767ddd58555d7204
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.7

Release files / unittest_fixtures-2.3.0-py3-none-any.whl

Download URL unittest_fixtures-2.3.0-py3-none-any.whl
Size 10.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cfd670240c4401010ca20f51bb363f181267eaea52713ab9464ad324243012d6
BLAKE2b-256 checksum
How to use checksums
f0a5a224c0dba1601f1fe57b212cf433f8e33491273646f8b8c809d5924ea74b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.7

Security for unittest-fixtures 2.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page