Skip to content

Instantly share code, notes, and snippets.

@drocco007
Last active January 11, 2016 12:20
Show Gist options
  • Save drocco007/6288869 to your computer and use it in GitHub Desktop.
Save drocco007/6288869 to your computer and use it in GitHub Desktop.
Short version: what is the best way to create fixture factories?

I'm working on a user search feature and would like some feedback about the best way to structure the creation of test data using pytest. Below I have defined a test file, test_basic.py, that defines fixture functions to create users and add them to my "database" of users:

@pytest.fixture
def user_alice():
    alice = User('Alice')
    users.append(alice)
    return alice

Tests set the state of the user database by requesting user objects as parameters, which will trigger the corresponding pytest fixture functions. Here is a test that shows the meat of this: the test requests the user Alice, which is created by the fixture and can be used to test the search function:

def test_can_find_alice_by_name(user_alice):
    assert user_alice in search('Alice')

I have tried to strip these examples down to the bare essentials, but even in the simple case of test_basic.py there is a lot of repetition to set up each user. As I expand the user fields I am able to search, I'll want to create more users and more combinations users, which means adding more fixture functions, all of which do the same thing with relatively minor variations in the data.

I'd appreciate your feedback on my proposed solution below, which defines a fixture factory that knows how to create users. The idea is this: for each test, we look to see if the test is requesting any users by checking its parameters for names that start with user_. If we find any, create the requested user object and pass it back to the test:

# excerpt from test_factory.py; factory fixture replaces _all_ the user
# fixtures in test_basic

#
# Factory that creates Users requested by a test case.  Test cases request a
# User by specifying a parameter name like 'user_<slug>'.  The User instance
# is created and passed back to the test using the requested name.  Any number
# of Users may be requested this way.
#

@pytest.fixture(autouse=True)
def user_factory_fixture(request):
    # We only want test case parameters that start with 'user_'
    _users = filter(lambda f: f.startswith('user_'), request.funcargnames)

    for user_slug in _users:
        # Create the user
        name = user_slug.split('_')[1].capitalize()
        user = User(name)
        users.append(user)

        # Pass the user back to the test
        request._funcargs[user_slug] = user

I appreciate any thoughts, suggestions, or advice you might have about this problem generally. While I'm using search as an example, I have run into this same problem in other contexts and am trying to learn the best practices for test environment and data management.

Telling me "you're going about this the completely wrong way" is a perfectly legitimate answer if you are willing to tell me why! :-)

#
# Simulated application code for user search
#
from collections import namedtuple
User = namedtuple('User', ['name'])
users = []
def search(name=None):
return [user for user in users if name in user.name]
import pytest
#
# Simulated test isolation fixture: ensure that each test starts in a
# consistent state. For this example, just clear the users list after
# each.
#
@pytest.fixture(autouse=True)
def cleanup(request):
def _clear():
del users[:]
request.addfinalizer(_clear)
#
# User object fixtures for users Alice, Bob, and Charlie. Each fixture creates
# a User object, adds it to the users list, and returns the instance.
#
@pytest.fixture
def user_alice():
alice = User('Alice')
users.append(alice)
return alice
@pytest.fixture
def user_bob():
bob = User('Bob')
users.append(bob)
return bob
@pytest.fixture
def user_charlie():
charlie = User('Charlie')
users.append(charlie)
return charlie
#
# Search tests. Each specifies which users are available by naming them as a
# parameter.
#
def test_can_find_alice_by_name(user_alice):
assert user_alice in search('Alice')
def test_should_not_find_bob(user_alice, user_bob):
assert user_bob not in search('Alice')
def test_should_find_substring_match(user_alice, user_bob, user_charlie):
results = search('li')
assert user_alice in results
assert user_charlie in results
def test_should_not_find_substring_mismatch(user_alice, user_bob, user_charlie):
results = search('li')
assert user_bob not in results
# etc.
#
# Simulated application code for user search
#
from collections import namedtuple
User = namedtuple('User', ['name'])
users = []
def search(name=None):
return [user for user in users if name in user.name]
import pytest
#
# Simulated test isolation fixture: ensure that each test starts in a
# consistent state. For this test, just clear the users list after each.
#
@pytest.fixture(autouse=True)
def cleanup(request):
def _clear():
del users[:]
request.addfinalizer(_clear)
#
# Factory that creates Users requested by a test case. Test cases request a
# User by specifying a parameter name like 'user_<slug>'. The User instance
# is created and passed back to the test using the requested name. Any number
# of Users may be requested this way.
#
@pytest.fixture(autouse=True)
def user_factory_fixture(request):
# We only want test case parameters that start with 'user_'
_users = filter(lambda f: f.startswith('user_'), request.funcargnames)
for user_slug in _users:
# Create the user
name = user_slug.split('_')[1].capitalize()
user = User(name)
users.append(user)
# Pass the user back to the test
request._funcargs[user_slug] = user
#
# Search tests. Each specifies which users are available by naming them as a
# parameter.
#
def test_can_find_alice_by_name(user_alice):
assert user_alice in search('Alice')
def test_should_not_find_bob(user_alice, user_bob):
assert user_bob not in search('Alice')
def test_should_find_substring_match(user_alice, user_bob, user_charlie):
results = search('li')
assert user_alice in results
assert user_charlie in results
def test_should_not_find_substring_mismatch(user_alice, user_bob, user_charlie):
results = search('li')
assert user_bob not in results
# etc.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment