
In this post I will demostrate how to use pytest fixtures to access the expected responses to several unit tests with the goal of reducing code duplication.
I have used VS Code to build and run these tests. If you need help setting up VS Code to run pytest take a look at this earlier post. The examples used within this post can be found on Github.
Overview of the code under test.
A Python program which solves the Fizzbuzz programming challenge in two different ways.
"""
Program to showcase different ways to solve the FizzBuzz coding challenge.
Each function will
Count from 1 - 100 inclusive
For numbers divisible by 3, Add to the list the string fizz
For numbers divisible by 5, Add to the list the string buzz
For numbers divisible by both 3 and 5, Add to a list the string fizzbuzz
For all other numbers add the number to the list and return it to the caller
"""
def fizz_buzz_using_boolean():
"""The results of the mod calculations are set to booleans
which makes the if statements easier to understand"""
fizz_buzz_list = []
for x in range(1, 101):
fizz = x % 3 == 0
buzz = x % 5 == 0
if fizz and buzz:
fizz_buzz_list.append('fizzbuzz')
continue
elif fizz:
fizz_buzz_list.append('fizz')
continue
elif buzz:
fizz_buzz_list.append('buzz')
continue
else:
fizz_buzz_list.append(x)
return fizz_buzz_list
def fizz_buzz_if_calc():
"""Fizzbuzz solution with the mod operator used within the
if statements"""
fizz_buzz_list = []
for x in range(1, 101):
if x % 3 == 0 and x % 5 == 0:
fizz_buzz_list.append("fizzbuzz")
continue
elif x % 3 == 0:
fizz_buzz_list.append("fizz")
continue
elif x % 5 == 0:
fizz_buzz_list.append("buzz")
continue
else:
fizz_buzz_list.append(x)
return fizz_buzz_list
if __name__ == "__main__":
fb_list_2 = fizz_buzz_using_boolean()
print(fb_list_2)
fb_list_1 = fizz_buzz_if_calc()
print(fb_list_1)
Unit tests without fixtures
For each function I want to ensure that there are 100 elements in the list and the list contains the correct answers to the Fizzbuzz challenge.
A first attempt looks like this
import pytest
from fizz_buzz import fizz_buzz_if_calc, fizz_buzz_using_boolean
def test_fizz_buzz_if_calc_has_100_elements():
actual = fizz_buzz_if_calc()
assert len(actual) == 100
def test_fizz_buzz_using_boolean_has_100_elements():
actual = fizz_buzz_using_boolean()
assert len(actual) == 100
def test_fizz_buzz_if_calc_has_correct_items():
actual = fizz_buzz_if_calc()
assert actual == [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz', 16, 17, 'fizz', 19, 'buzz', 'fizz', 22, 23, 'fizz', 'buzz', 26, 'fizz', 28, 29, 'fizzbuzz', 31, 32, 'fizz', 34, 'buzz', 'fizz', 37, 38, 'fizz', 'buzz', 41, 'fizz', 43, 44, 'fizzbuzz', 46, 47, 'fizz', 49, 'buzz', 'fizz', 52, 53, 'fizz', 'buzz', 56, 'fizz', 58, 59, 'fizzbuzz', 61, 62, 'fizz', 64, 'buzz', 'fizz', 67, 68, 'fizz', 'buzz', 71, 'fizz', 73, 74, 'fizzbuzz', 76, 77, 'fizz', 79, 'buzz', 'fizz', 82, 83, 'fizz', 'buzz', 86, 'fizz', 88, 89, 'fizzbuzz', 91, 92, 'fizz', 94, 'buzz', 'fizz', 97, 98, 'fizz', 'buzz']
def test_fizz_buzz_using_boolean_has_correct_items():
actual = fizz_buzz_using_boolean()
assert actual == [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz', 16, 17, 'fizz', 19, 'buzz', 'fizz', 22, 23, 'fizz', 'buzz', 26, 'fizz', 28, 29, 'fizzbuzz', 31, 32, 'fizz', 34, 'buzz', 'fizz', 37, 38, 'fizz', 'buzz', 41, 'fizz', 43, 44, 'fizzbuzz', 46, 47, 'fizz', 49, 'buzz', 'fizz', 52, 53, 'fizz', 'buzz', 56, 'fizz', 58, 59, 'fizzbuzz', 61, 62, 'fizz', 64, 'buzz', 'fizz', 67, 68, 'fizz', 'buzz', 71, 'fizz', 73, 74, 'fizzbuzz', 76, 77, 'fizz', 79, 'buzz', 'fizz', 82, 83, 'fizz', 'buzz', 86, 'fizz', 88, 89, 'fizzbuzz', 91, 92, 'fizz', 94, 'buzz', 'fizz', 97, 98, 'fizz', 'buzz']
The highlighted lines show the duplication within the tests. The literals for the expected number of elements as well as the list with the expected answers are all referenced more than once. This is a problem because when the code changes, it will mean more effort for the tests to pass again. From experience tests that require more effort to maintain are the first to be disabled.
Refactoring unit tests one
In the context of this post a fixture is a regular Python function with the addition of a @pytest.fixture decorator. Below is the fixture that the tests will use to check the correct number of elements.
@pytest.fixture
def expected_number_of_elements():
"""The fizzbuzz list will have 100 elements"""
return 100
To use the fixture, the changes to the unit tests are highlighted below
def test_fizz_buzz_if_calc_has_100_elements(expected_number_of_elements):
actual = fizz_buzz_if_calc()
assert len(actual) == expected_number_of_elements
def test_fizz_buzz_using_boolean_has_100_elements(expected_number_of_elements):
actual = fizz_buzz_using_boolean()
assert len(actual) == expected_number_of_elements
The test_fizz_buzz_if_calc_has_100_elements function now has a parameter which is the name of fixture function, expected_number_of_elements. The assert statement now compares the actual value with the expected value returned by the fixture. If the code changes to return more or less elements there is one place that needs to change.
Refactoring unit tests two
The tests which ensure the list returned by the functions are the correct answers to the Fizzbuzz challenge are also changed to use fixtures. A new fixture with the name fizzbuzz_expected_answer is created
@pytest.fixture
def fizzbuzz_expected_answer():
"""A list of the fizzbuzz answers for 1 - 100"""
fizz_buzz = [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz', 16, 17, 'fizz', 19, 'buzz', 'fizz', 22, 23, 'fizz', 'buzz', 26, 'fizz', 28, 29, 'fizzbuzz', 31, 32, 'fizz', 34, 'buzz', 'fizz', 37, 38, 'fizz', 'buzz', 41, 'fizz', 43, 44, 'fizzbuzz', 46, 47, 'fizz', 49, 'buzz', 'fizz', 52, 53, 'fizz', 'buzz', 56, 'fizz', 58, 59, 'fizzbuzz', 61, 62, 'fizz', 64, 'buzz', 'fizz', 67, 68, 'fizz', 'buzz', 71, 'fizz', 73, 74, 'fizzbuzz', 76, 77, 'fizz', 79, 'buzz', 'fizz', 82, 83, 'fizz', 'buzz', 86, 'fizz', 88, 89, 'fizzbuzz', 91, 92, 'fizz', 94, 'buzz', 'fizz', 97, 98, 'fizz', 'buzz']
return fizz_buzz
To use the fixture, the changes to the unit tests are highlighted below
def test_fizz_buzz_if_calc_has_correct_items(fizzbuzz_expected_answer):
actual = fizz_buzz_if_calc()
assert actual == fizzbuzz_expected_answer
def test_fizz_buzz_using_boolean_has_correct_items(fizzbuzz_expected_answer):
actual = fizz_buzz_using_boolean()
assert actual == fizzbuzz_expected_answer
Similar to the refactoring of of the prior unit tests, these tests now have a parameter which is the name of fixture function, fizzbuzz_expected_answer and can then be used within the assert statement to compare the actual and expected values.
The unit tests with fixtures
The complete unit test is
import pytest
from fizz_buzz import fizz_buzz_if_calc, fizz_buzz_using_boolean
@pytest.fixture
def expected_number_of_elements():
"""The fizzbuzz list will have 100 elements"""
return 100
@pytest.fixture
def fizzbuzz_expected_answer():
"""A list of the fizzbuzz answers for 1 - 100"""
fizz_buzz = [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz', 16, 17, 'fizz', 19, 'buzz', 'fizz', 22, 23, 'fizz', 'buzz', 26, 'fizz', 28, 29, 'fizzbuzz', 31, 32, 'fizz', 34, 'buzz', 'fizz', 37, 38, 'fizz', 'buzz', 41, 'fizz', 43, 44, 'fizzbuzz', 46, 47, 'fizz', 49, 'buzz', 'fizz', 52, 53, 'fizz', 'buzz', 56, 'fizz', 58, 59, 'fizzbuzz', 61, 62, 'fizz', 64, 'buzz', 'fizz', 67, 68, 'fizz', 'buzz', 71, 'fizz', 73, 74, 'fizzbuzz', 76, 77, 'fizz', 79, 'buzz', 'fizz', 82, 83, 'fizz', 'buzz', 86, 'fizz', 88, 89, 'fizzbuzz', 91, 92, 'fizz', 94, 'buzz', 'fizz', 97, 98, 'fizz', 'buzz']
return fizz_buzz
def test_fizz_buzz_if_calc_has_100_elements(expected_number_of_elements):
actual = fizz_buzz_if_calc()
assert len(actual) == expected_number_of_elements
def test_fizz_buzz_using_boolean_has_100_elements(expected_number_of_elements):
actual = fizz_buzz_using_boolean()
assert len(actual) == expected_number_of_elements
def test_fizz_buzz_if_calc_has_correct_items(fizzbuzz_expected_answer):
actual = fizz_buzz_if_calc()
assert actual == fizzbuzz_expected_answer
def test_fizz_buzz_using_boolean_has_correct_items(fizzbuzz_expected_answer):
actual = fizz_buzz_using_boolean()
assert actual == fizzbuzz_expected_answer
Is this a correct use of fixtures?
I am still finding my way with fixtures so have asked over on Code Review if using fixtures to obtain the expected answers to a unit test is an acceptable use case.