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.