Last Updated: February 25, 2016
·
3.865K
· deontologician

Function factories for random data with py.test

Py.test has an excellent facility called fixtures, which are a pythonic way of doing dependency injection.

Quick example:

from random import randint
import pytest

@pytest.fixture
def number():
    return randint(10)

@pytest.fixture
def structure(number):
    return {'name': 'something',
            'number': number}

def test_something(structure, number):
    assert structure['number'] == number

The random number will only be generated once, and during the test, it will always be that number wherever it is injected. So here the structure fixture sees the same number as test_something. But if we run it again, a new number will be generated.

This is great if you want to generate a single piece of random data, and check it later. But often you actually need several pieces of random data, and you don't want to create fixtures like random_number1, random_number2 etc.

A nice strategy that I've found works is instead of having the fixture return the data directly, instead the fixture returns a function that has access to other fixtures, and that keeps track of what it's generated.

This looks something like the following example:

@pytest.fixture
def number():
    return randint(10)

@pytest.fixture
def structure(number):
    def _structure(name):
        '''This function gets injected'''
        retval = {
            'name': name,
            'number': randint(number, 20)
        }
        # register the result in the function
        _structure.results[name] = retval
        return retval
    # before injecting, we create the registry
    # on the function
    _structure.results = {}
    return _structure

@pytest.fixture
def big_structure(structure):
    return {
        'foo': structure('bar')
    }

The key points are:

  1. Have the fixture return a function, instead of the data
  2. The function gets access to any other fixtures you want (here we use the number fixture as a floor for a new random number.
  3. Add a registry attribute to the function, to keep track of results
def test_something(structure, number, big_structure):
    result = structure('some_name')
    assert result['number'] >= number
    assert structure.results['some_name'] == result
    assert structure.results['bar'] == big_structure['foo']

Why does the _structure function register its own results? Because you may need to use the structure fixture in other fixtures. Here big_structure uses structure and if we want to know what was generated, we can just look in the registry.

Additionally, since the function is created per test the registry is automatically cleared out for every test.