Python Friday #49: Creating Your Own Fixtures for Pytest

While built-in fixtures are a great help, they cannot address the specific needs of our applications. It is now time to have a closer look on how to create our own fixtures.

This post is part of my journey to learn Python. You can find the other parts of this series here.

 

Why not just setup and teardown?

Other testing frameworks have the concept of setup and teardown methods to get your system in a known state. They can be per class or sometimes even for the whole test suite (like SetUpFixture for NUnit in .Net). The downside with this approach is that the setup part is often too specific to reuse it, or it runs for tests that do not need it.

With fixtures in pytest you control where they run and what they do. You can use a fixture in a test that needs it and not use it in the other tests in the same file. Since a fixture is not bound to a file/module, you can use it throughout your test suite.

 

Reduce duplication

If we have tests that need the same setup code, we will end up with a lot of duplication. The bigger and more complex the creation of our objects get, the more code we write in all our tests.

For the examples on fixtures I use the Phonebook class from the Pluralsight course “Unit Testing with Python” by Emily Bache extended with some additional complexity. The code we want to test is in the file contact.py:

The two tests I have use half of their method to create a Phonebook instance:

We can move the duplicated code in its own function and use the decorator @pytest.fixture to tell pytest that this is a fixture:

As with the built-in ones, we can now pass this fixture as an argument to our test functions and get rid of the duplication:

 

Clean-up after our fixture

Often we need to do some clean-up after we run a test. While we could create another fixture, pytest has a better approach: we can use the yield statement in our fixture to turn it into a generator (as explained in the last post ). This means pytest can leverage the features of Python and our code is simpler to understand when the setup and clean-up code is in the same place:

 

Scopes for our fixture

By default, pytest creates an instance of our fixture for every test function:

===================== test session starts ======================
platform win32 — Python 3.8.1, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: D:\Python
plugins: cov-2.10.1, html-2.1.1, metadata-1.10.0
collected 2 items

test_own_fixture.py Phonebook created
.Phonebook removed
Phonebook created

.Phonebook removed

We can change this behaviour with a scope-argument to the decorator. Valid values for scope are “function” (default), “class”, “module” and “session”.

===================== test session starts ======================
platform win32 — Python 3.8.1, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: D:\Python
plugins: cov-2.10.1, html-2.1.1, metadata-1.10.0
collected 2 items

test_own_fixture.py Phonebook created
..Phonebook removed

 

Using fixtures inside other fixtures

We can use fixtures not only in our test functions, but also in our fixtures. If for example our phonebook needs a temporary directory to save its entries, we can use tmpdir as a parameter:

However, there is a little catch we need to understand: All fixtures must have the same scope. If we want to scope our own fixture to span the whole module and we use tmpdir that has the scope set to function, we will get an error like this:


================== ERRORS ==================
_______ ERROR at setup of test_add_contact_to_phonebook ________
ScopeMismatch: You tried to access the ‘function’ scoped fixture ‘tmpdir’ with a ‘module’ scoped request object, involved factories
test_own_fixture.py:19: def phonebook(tmpdir)
__

 

Reuse our fixtures

The great benefit of fixtures is that we can reuse them in all our tests. However, if we try to use it in another file, we get this error:

__ ERROR at setup of test_add_multiple_contacts_to_phonebook ___
file D:\Python\test_own_fixture2.py, line 3
def test_add_multiple_contacts_to_phonebook(phonebook):
E fixture ‘phonebook’ not found
> available fixtures: cache, capfd, capfdbinary, caplog,…

The dependency injection part of pytest does not know where our fixture comes from. If we run all our tests it could be found, but what happens if we only want to run one test file? The solution to this problem is that we move our fixtures in a file called conftest.py directly in the test folder:

Now pytest finds your fixture independently of what test you run:

===================== test session starts ======================
platform win32 — Python 3.8.1, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: D:\Python
plugins: cov-2.10.1, html-2.1.1, metadata-1.10.0
collected 1 item

test_own_fixture2.py Phonebook created
.Phonebook removed

====================== 1 passed in 0.03s =======================

 

List your own fixtures

The more you create fixtures, the less you remember exactly how you named them. Pytest helps us to find what we need by including our own fixtures in the output of –fixtures:


———— fixtures defined from conftest————
phonebook
      conftest.py:5: no docstring available

As a last change on our fixture we should add a little bit of documentation:

This little change will make working with our fixture a lot simpler:


———— fixtures defined from conftest————
phonebook
      Creates a Phonebook instance

 

Next

Now that we can create our fixtures it is time to look at markers and how we can use them to run a subset of our tests.

2 thoughts on “Python Friday #49: Creating Your Own Fixtures for Pytest”

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.