Reducing duplication with pytest fixtures

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.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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