Fixtures and Parameters: Testing Code with Pytest

iGenius
Ideas @ iGenius
Published in
4 min readOct 16, 2018

When it comes to testing if your code is bulletproof, pytest fixtures and parameters are precious assets. Our back-end dev Vittorio Camisa explains how to make the best of them.

Pytest, unlike the xUnit family such as unittest, does not have classical setup or teardown methods and test classes. The pytest approach is more flat and simple, and it mainly requires the usage of functions and decorators.

Fixtures

The first and easiest way to instantiate some dataset is to use pytest fixtures.

pytest.fixture decorator makes it possible to inject the return value in the test functions whose have in their signature the decorated function name.
It’s really more hard to figure out than just seeing it in action:

def get_gender_heading(name):
# super AI based algorithm (such 2018, much AI)
# Return "Mr" or "Mrs" for a given name.
@pytest.fixture
def male_name():
return 'Phil'
@pytest.fixture
def female_name():
return 'Claire'
def test_male_prefix(male_name):
assert 'Mr.' == get_gender_heading(male_name)
def test_female_prefix(female_name):
assert 'Mrs.' == get_gender_heading(female_name)

Easy, isn’t it? You can potentially generate and create everything you need in these fixture-functions and then use it in all the tests you need.

Of course, you can combine more than one fixture per test:

def test_both_sex(female_name, male_name):
assert 'Mr.' == get_gender_heading(male_name)
assert 'Mrs.' == get_gender_heading(female_name)
# Yes, this is not a good test :)

Moreover, fixtures can be used in conjunction with the yield for emulating the classical setup/teardown mechanism:

@pytest.fixture
def male_name():
print('Executing setup code')
yield 'Jay'
print('Executing teardown code')

Still not satisfied? Make this fixture run without any test by using autouse parameter. This is particularly helpful when patching/mocking functions:

@pytest.fixture(autouse=True, scope='function')
def common_patches():
mock.patch(...)

This is still not enough for some scenarios. Let’s suppose you want to test your code against a set of different names and actions: a solution could be iterating over elements of a “list” fixture.

@pytest.fixture
def male_names():
return ['Phil', 'Jay', 'Luke', 'Manny']
def test_male_prefix_v2(male_names):
for name in male_names:
assert 'Mr.' == get_gender_heading(name)

But there are far better alternatives with pytest, we are getting there :)

Parametrized tests

pytest.mark.parametrize to the rescue!
The above decorator is a very powerful functionality, it permits to call a test function multiple times, changing the parameters input at each iteration.
The first argument lists the decorated function’s arguments, with a comma separated string. The second argument is an iterable for call values.

Let’s see how it works.

@pytest.mark.parametrize('name', ['Claire', 'Gloria', 'Haley'])
def test_female_prefix_v2(name):
assert 'Mrs.' == get_gender_heading(name)

So, pytest will call test_female_prefix_v2 multiple times: first with name='Claire', then with name='Gloria' and so on.
This is especially useful when using multiple args at time:

@pytest.mark.parametrize(
'name, expected', [('Claire', 'Mrs'), ('Jay', 'Mr')]
)
def test_both_sex_v2(name, expected):
assert expected == get_gender_heading(name)

With multiple arguments,pytest.mark.parametrize will perform a simple association based on index, so whilename will assume first Claire and then Jay values, expected will assume Mrs and Mrvalues.

Another thing the parametrize is good for is making permutations.
In fact, using more than one decorator, instead of a single one with multiple arguments, you will obtain every permutation possible.

def is_odd(number):
return number % 2 != 0
@pytest.mark.parametrize('odd', range(1, 11, 2))
@pytest.mark.parametrize('even', range(0, 10, 2))
def test_sum_odd_even_returns_odd(odd, even):
assert is_odd(odd + even)
Pytest will here do all the permutation with the two series.

Again, it might not be enough if those permutations are needed in a lot of different tests

@pytest.mark.parametrize('odd', range(1, 11, 2))
@pytest.mark.parametrize('even', range(0, 10, 2))
def test_sum_odd_even_returns_odd(odd, even):
assert is_odd(odd + even)
@pytest.mark.parametrize('odd', range(1, 11, 2))
@pytest.mark.parametrize('even', range(0, 10, 2))
def another_meaningful_test(odd, even):
assert ...
@pytest.mark.parametrize('odd', range(1, 11, 2))
@pytest.mark.parametrize('even', range(0, 10, 2))
def test_important_piece_of_code(odd, even):
assert ...
@pytest.mark.parametrize('odd', range(1, 11, 2))
@pytest.mark.parametrize('even', range(0, 10, 2))
def test_im_out_of_names(odd, even):
assert ...

That’s a lot of lines of code; furthermore, in order to change the range, you’d have to modify each decorator manually! Collapsing them would definitely speed up your work. This brings us to the next feature of pytest.

Parametrized fixture

Back to origins: fixtures.
Quoting the pytest documentation

Fixture functions can be parametrized in which case they will be called multiple times, each time executing the set of dependent tests, i. e. the tests that depend on this fixture. Test functions do usually not need to be aware of their re-running. Fixture parametrization helps to write exhaustive functional tests for components which themselves can be configured in multiple ways.

Brilliant! That’s exactly what we want. Let’s see it in action:

@pytest.fixture(params=range(1, 11, 2))
def odd(request):
return request.param
@pytest.fixture(params=range(0, 10, 2))
def even(request):
return request.param
def test_sum_odd_even_returns_odd(odd, even):
assert is_odd(odd + even)
def another_meaningful_test(odd, even):
assert ...
def test_important_piece_of_code(odd, even):
assert ...
def test_im_out_of_names(odd, even):
assert ...

This achieves the same goal but the resulting code is far, far better!
This flavor of fixtures allows to cover a lot of edge cases in multiple tests with minimum redundancy and effort, keeping the test code very neat and clean.

Wrapping up

Fixtures are useful to keep handy datasets and to patch/mock code, before and after test execution. With params parameter you have also the possibility to run different flavors of the same fixture for each test that uses it, without any other change needed.

Parametrize will help you in scenarios in which you can easily say “given these inputs, I expect that output”.

Both these features are very simple yet powerful. Here at iGenius we are having a very good experience using them in our tests.

Useful links

https://docs.pytest.org/en/latest/fixture.html
https://docs.pytest.org/en/latest/fixture.html#parametrizing-fixtures
https://docs.pytest.org/en/latest/parametrize.html

--

--