In pytest xUnit style fixtures, I presented a problem where:
- Two tests exist in a test file.
- One uses a resource.
- The other doesn’t.
- Module level fixtures don’t work if you just want to run the one function that doesn’t use the resource.
I then presented class level fixtures as a way to solve the separation problem.
In this post, I’ll use pytest fixtures to solve the same problem.
I’m not going into details of all the goodies you get with pytest fixtures.
I’ll just stick to solving this problem.
In future posts, I’ll go into more details about pytest fixtures.
- the problem
- the pytest fixture solution
- some benefits of pytest fixtures
- an even smaller example
- wrap up
the problem
Here’s the code that caused us trouble last time:
from __future__ import print_function
def resource_a_setup():
print('resources_a_setup()')
def resource_a_teardown():
print('resources_a_teardown()')
def setup_module(module):
print('\nsetup_module()')
resource_a_setup()
def teardown_module(module):
print('\nteardown_module()')
resource_a_teardown()
def test_1_that_needs_resource_a():
print('test_1_that_needs_resource_a()')
def test_2_that_does_not():
print('\ntest_2_that_does_not()')
The problem is that if I want to just run ‘test_2_that_does_not()’, the fixture functions for resource_a are called, even though I don’t need them to be called.
`
$ py.test -s -v test_realistic_two_funcs.py::test_2_that_does_not ================= test session starts ================== platform darwin -- Python 2.7.5 -- pytest-2.3.4 -- /usr/bin/python collected 3 items test_realistic_two_funcs.py:20: test_2_that_does_not setup_module() resources_a_setup() test_2_that_does_not() PASSED teardown_module() resources_a_teardown() =============== 1 passed in 0.01 seconds ===============
`
the pytest fixture solution
Instead of moving the resource_a related fixtures and tests into a class, we:
- Import pytest
- Use the pytest fixture decorator to specify ‘resource_a_setup()’ as a fixture.
- Specify the fixture as module scope, so if two tests need it, it will still only have setup/teardown called once.
- Specify ‘resource_a_teardown()’ as a finalizer for ‘resource_a_setup()’. To do this, we need to add a ‘request’ param to the setup function. Also, note that the finalizer function can be very local to the setup function, even defined within it.
- Include ‘resource_a_setup’ in the param list for tests that use resource_a.
I’ll also add one more test function that uses the resource, to prove that module scope works.
For pytest fixtures to work, steps #1, #2 and #5 are all that are really needed.
Step #3 is only needed if you want to modify the default (which is ‘function’).
Step #4 is only needed if you want to include a teardown function.
So, here’s my code.
from __future__ import print_function
import pytest
@pytest.fixture(scope='module')
def resource_a_setup(request):
print('\nresources_a_setup()')
def resource_a_teardown():
print('\nresources_a_teardown()')
request.addfinalizer(resource_a_teardown)
def test_1_that_needs_resource_a(resource_a_setup):
print('test_1_that_needs_resource_a()')
def test_2_that_does_not():
print('\ntest_2_that_does_not()')
def test_3_that_does(resource_a_setup):
print('\ntest_3_that_does()')
Running only ‘test_2_that_does_not’:
$ py.test -s -v test_three_funcs.py::test_2_that_does_not
================= test session starts ==================
platform darwin -- Python 2.7.5 -- pytest-2.3.4 -- /usr/bin/python
collected 4 items
test_three_funcs.py:14: test_2_that_does_not
test_2_that_does_not()
PASSED
=============== 1 passed in 0.01 seconds ===============
Running everything:
$ py.test -s -v test_three_funcs.py
================= test session starts ==================
platform darwin -- Python 2.7.5 -- pytest-2.3.4 -- /usr/bin/python
collected 3 items
test_three_funcs.py:11: test_1_that_needs_resource_a
resources_a_setup()
test_1_that_needs_resource_a()
PASSED
test_three_funcs.py:14: test_2_that_does_not
test_2_that_does_not()
PASSED
test_three_funcs.py:17: test_3_that_does
test_3_that_does()
PASSED
resources_a_teardown()
=============== 3 passed in 0.01 seconds ===============
some benefits of pytest fixtures
Right away we can see some cool benefits.
- It’s obvious which tests are using a resource, as the resource is listed in the test param list.
- I don’t have to artificially create classes (or move tests from one file to another) just to separate fixture usage.
- The teardown code is tightly coupled with the setup code for one resource.
- Scope for the lifetime of the resource is specified at the location of the resource setup code. This ends up being a huge benefit when you want to fiddle with scope to save time on testing. If everything starts going haywire, it’s a one line change to specify function scope, and have setup/teardown run around every function/method.
- It’s less code. The pytest solution is smaller than the class solution.
an even smaller example
I stated earlier in the solution, that steps #3 and #4 are optional.
Let’s take a look at the simplified code if we just go with the defaults.
from __future__ import print_function
import pytest
@pytest.fixture()
def resource_a():
print('\nresources_a() "setup"')
def test_1_that_needs_resource_a(resource_a):
print('test_1_that_needs_resource_a()')
def test_2_that_does_not():
print('\ntest_2_that_does_not()')
def test_3_that_does(resource_a):
print('test_3_that_does()')
The difference?
- No teardown code (finalizer). So no need for a request param for the setup func.
- No scope specified. The default will call ‘resource_a’ before every func/method that needs it.
- Oh yeah. I also shortened the resource name. Dropping off the ‘_setup’.
This shortened version is more typical of how I would start writing my test code.
I only add finalizers (teardown) if necessary for the resource.
It is cool to note that only the resource fixture has to care about the finalizer.
You can add it if you need to, and the change needed is only to the setup fixture code.
I also usually am ok with function level scoping at first.
I pay attention to run times and realistic needed scoping for resources, and fiddle with scope if necessary.
And again, this fiddling is isolated to the resource fixture code.
The tests don’t have to change to support different scoping.
So, here’s my test run:
$ py.test -s -v test_three_funcs_small.py
================= test session starts ==================
platform darwin -- Python 2.7.5 -- pytest-2.3.4 -- /usr/bin/python
collected 3 items
test_three_funcs_small.py:8: test_1_that_needs_resource_a
resources_a() "setup"
test_1_that_needs_resource_a()
PASSED
test_three_funcs_small.py:11: test_2_that_does_not
test_2_that_does_not()
PASSED
test_three_funcs_small.py:14: test_3_that_does
resources_a() "setup"
test_3_that_does()
PASSED
=============== 3 passed in 0.03 seconds ===============
wrap up
My main goal for this post was to show that using pytest fixtures is at least as easy as using the class fixture solution to separate fixture usage.
I hope I’ve demonstrated that.
Please let me know if I’ve left some questions open.
There’s a lot more to cover, and I have a general plan for where to go from here, but I’d love your input.