Pytest Test Parametrization

pytest supports a feature called test parametrization. Parametrization blew me away the first time I saw it in a test our team lead had written. According to the docs, it “allows you to define multiple sets of arguments and fixtures at the test function.” That explanation isn’t very easy to understand right away so I’ll try and explain it using an example.

The Customer object.

I would like you to imagine for a second that we have a Django application that stores customer information. Customers are represented in the application as a database model:


class Customer(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()
    phone = models.CharField(max_length=30)
    country = models.CharField(max_length=30)

Prepare data for the test

When testing the customer information application, one of the unit tests you’d want to write is a test that checks that each new customer object has the correct fields. First, you create the setup or fixture. A fixture is a function that pytest runs before running the tests. In this case, the fixture sets up a new Customer object that’ll be used in the test:

@pytest.fixture()
def customer():
    return Customer(
        name="Jack",
        age=24,
        country="Germany",
        phone="555-123-4567",
    )

Test all the things

The next step is to write a test that asserts that the customer object created in the fixture (and all subsequent objects that will be created by the Customer model) has all the fields it should have.

The test looks something like this:

class TestCustomerModel:
    def test_account_has_correct_fields(self, customer):
        assert hasattr(customer, 'name')
        assert hasattr(customer, 'age')
        assert hasattr(customer, 'country')
        assert hasattr(customer, 'phone')

This is a valid test that’ll fail if any of the checks fail. As you can see, there is a lot of repetition in that test and if your model is complicated the test can become unreadable very quickly. Some members of the development community feel that having multiple assert statements in a single test is a bad idea because a unit test is supposed to test only one thing. One way to fix this is to put each assert in its own test:

class TestCustomerModel:
    def test_account_has_name_field(self, customer):
        assert hasattr(customer, 'name')

    def test_account_has_age_field(self, customer):
        assert hasattr(customer, 'age')

    def test_account_has_country_field(self, customer):
        assert hasattr(customer, 'country')

    def test_account_has_phone_field(self, customer):
        assert hasattr(customer, 'phone')

Parametrize the tests

That looks a little better, each test tests only one thing and follows best practice. The problem is that these tests are all very similar, each of them checks if the Customer object has a specific field as an attribute. There must be a way to test all the object’s fields without typing out a different test by hand. This is where test parametrization comes in. When a test is parametrized, multiple sets of data are sent to it and pytest reports if any of the sets failed:

class TestCustomerModelParameterized:

    @pytest.mark.parametrize("field_name", ("name", "age", "phone", "country"))
    def test_account_has_correct_field(self, field_name, customer):
        assert hasattr(customer, field_name)

The code above works the same as the code you saw before, the only difference is that in the latter snippet, you write only one test function and have pytest take care of passing the different fields that should be tested against the customer object to the test. This avoids repetition and saves you on a lot of typing.

When you run pytest on the above test, you’ll notice that it collects four separate tests, each testing a different argument.

$ pytest test_parameterization.py -vv
============================= test session starts ==============================
platform linux -- Python 3.5.2, pytest-4.4.2, py-1.8.0, pluggy-0.11.0 -- /home/terra/.virtualenvs/test-venv/bin/python3
cachedir: .pytest_cache
rootdir: /home/terra/test/testdir_1

collected 4 items                                                              

test_parameterization.py::TestCustomerModelParameterized::test_account_has_correct_fields[name] PASSED [ 25%]
test_parameterization.py::TestCustomerModelParameterized::test_account_has_correct_fields[age] PASSED [ 50%]
test_parameterization.py::TestCustomerModelParameterized::test_account_has_correct_fields[phone] PASSED [ 75%]
test_parameterization.py::TestCustomerModelParameterized::test_account_has_correct_fields[country] PASSED [100%]

Conclusion

Parametrizing tests can help keep your code shorter and avoid repetition. The syntax takes a little getting used to at first, but once you get the hang of it, it’s a neat feature to use. For more information on how parametrization works and its advanced options, check out the docs. Thanks for reading. If you have any feedback, please feel free to drop me a comment below.