Pytest: Creating and Using Fixtures for Streamlined Testing

Written by Dan Sackett on September 8, 2014

As I've mentioned, pytest is a powerful testing suite for Python applications. What makes it more powerful are the fixtures.

In my last post about pytest, we learned the basics of the framework. If you really want to harness the power of pytest though, you need to create fixtures.

What are fixtures

In most testing frameworks, fixtures are commonplace. They're essentially objects that we can use in our tests. Ultimately, they provide a fixed baseline upon which tests can reliably and repeatedly execute. With pytest, we see fixtures that go beyond the typical setup and teardown functionality.

For instance:

Creating Fixtures

Let's create a fixture with pytest and see a few of these things in practice:

import pytest

@pytest.fixture()
def my_fixture():
    print "\nI'm the fixture"

As you can see, creating a fixture is fairly straightforward.

To use this fixture, we would do the following:

def test_my_fixture(my_fixture):
    print "I'm the test"

Take note that all we're doing to use this fixture is passing it as a parameter to the test function. When we run the test, we'll see that the fixture is called first and then the test. I'm going to use the py.test -s TEST_NAME command to send print statements to stdout.

$ py.test -s tests/test_funcs.py 
================================================= test session starts =================================================
platform linux2 -- Python 2.7.3 -- py-1.4.23 -- pytest-2.6.1
collected 1 items 

tests/test_funcs.py 
I'm the fixture
I'm the test
.

============================================== 1 passed in 0.15 seconds ===============================================

As we predicted, we see the print statements in the correct order.

Other Ways of Using pytest Fixtures

Pytest gives us a few other ways to use our fixtures. The standard parameter method is great and used most often, but we also have a usefixtures() decorator. The decorator can be used like so:

@pytest.mark.usefixtures('my_fixture')
def test_my_fixture():
    print "I'm the test"

We mark the test to use our fixture and the results are the same as before. Take note that you can pass multiple fixtures in by using comma-separated values. This method comes in handy within test classes.

@pytest.mark.usefixtures('my_fixture')
class Test:
    def test1(self):
        print "I'm the test 1"

    def test2(self):
        print "I'm the test 2"

You can see that we are using the fixture for the entire class and then each test within the class will use the fixture. It saves you time from marking all of your tests if they're going to be using the same fixture.

Another way to get this same effect on an entire test file is to set the pytestmark variable. For instance:

import pytest

pytestmark = pytest.mark.usefixtures('my_fixture')

def test_my_fixture():
    print "I'm the test"

class Test:
    def test1(self):
        print "I'm the test 1"

    def test2(self):
        print "I'm the test 2"

By doing this, we set the fixtures globally for this file and all test functions within it will use these fixtures. One thing to note is that all your functions may not need this fixture. If that's the case, it's better to directly specify each fixture for a test function rather than taking the lazy road and marking them all at the top. With larger fixtures, this can cause the tests to load slower as the fixtures are built.

The last way to use fixtures if by setting an autouse parameter in the fixture declaration. Using autouse can be great, but it can also be quite dangerous as mentioned in the last method. Autouse, unless confined to a scope, will run on all tests in the current session. This may not be what you're looking for and should be thought about before going ahead and trying it. I'll show you an example:

import pytest

@pytest.fixture(autouse=True)
def my_fixture():
    print "\nI'm the fixture"

def test_my_fixture():
    print "I'm the test"

class Test:
    def test1(self):
        print "I'm the test 1"

    def test2(self):
        print "I'm the test 2"

This will give us the same result as before, but remember that all tests in the current session will receive it. This includes other files and other functions.

Returning Values

Fixtures don't just have to print things. They are primarily used to return data for you to manipulate in tests. Just as a regular function, we can return something and then in our test we can make use of it.

import pytest

@pytest.fixture()
def my_fixture():
    data = {'x': 1, 'y': 2, 'z': 3}
    return data

def test_my_fixture(my_fixture):
    assert my_fixture['x'] == 1

Adding Finalizers

If you want to run something after a test with a fixture has completed, we can use finalizers. To do this, we get access to the request fixture from pytest. A finalizer is a function inside a fixture that will run after every test that a fixture is included.

@pytest.fixture()
def my_fixture(request):
    data = {'x': 1, 'y': 2, 'z': 3}

    def fin():
        print "\nMic drop"
    request.addfinalizer(fin)

    return data

The request fixture has an addfinalizer() method that can take in a function. Our function can simply print or we can disconnect from a database even. It gives you control over a fixture once a test has finished.

Scoping Fixtures

A lot of times we may have a fixture that we want to run on all functions or all classes. Pytest gives us a set of scope variables to define exactly when we want to use our fixture.

To use these, we define them like we did for autouse.

@pytest.fixture(scope="class")

The scope is defaulted to function. Where would you use each of these?

With scope, autouse can make sense sometimes. If the scope is set to session and autouse is True then we will have the fixtures run once at the start of the session and that's it. Play around with these things to see how it affects test time and overall results.

Using Fixture Information in Tests

Pytest provides access to the fixture information on the request object. These include things such as:

These are only a few as an example. Pytest has a list of all of them on their website.

Adding Parameters to Fixtures

Pytest gives us a cool way to use a single fixture multiple times. By passing a params=[] parameter into the fixture definition we can create multiple fixtures out of one. An example shows this best:

import pytest


@pytest.fixture(params=[
    # Tuples with password string and expected result
    ('password', False),
    ('p@ssword', False),
    ('p@ssw0rd', True)
])
def password(request):
    """Password fixture"""
    return request.param


def password_contains_number(password):
    """Checks if a password contains a number"""
    return any([True for x in range(10) if str(x) in password])


def password_contains_symbol(password):
    """Checks if a password contains a symbol"""
    return any([True for x in '!,@,#,$,%,^,&,*,(,),_,-,+,='.split(',') if x in password])


def check_password(password):
    """Check the password"""
    return password_contains_number(password) and password_contains_symbol(password)


def test_password_verifier_works(password):
    """Test that the password is verifyied correctly"""
    (input, result) = password
    print '\n'
    print input

    assert check_password(input) == result

And running the test yields:

$ py.test -s tests/test_funcs.py 
================================================= test session starts =================================================
platform linux2 -- Python 2.7.3 -- py-1.4.23 -- pytest-2.6.1
collected 3 items 

tests/test_funcs.py 

password
.

p@ssword
.

p@ssw0rd
.

============================================== 3 passed in 0.16 seconds ===============================================

We only declared one test but the test was run three times, each with different values. Parametrizing is super powerful and super awesome.

Conclusion

Pytest is smart about how you can use fixtures. It gives you a comprehensive API and tons of features that make writing and running tests much easier. You can use fixtures inside of fixtures, include many fixtures in a test, and so much more. In the end, your code remains more DRY by not repeating setup and teardowns.

Give them a try and let me know if you have any questions.


python pytest

comments powered by Disqus