Pytest Parameter Tests

Posted by Ryan Himmelwright on Thu, Oct 21, 2021
Tags pytest, testing, python, dev
Moses H. Cone Memorial Park, Blowing Rock, NC

Recently, I’ve been converting a small python script I wrote into an organized project. As part of that work, I started to setup an automated testing framework (like a good QE 😆). While writing tests, I have been parameterizing the code so that each test function can be provided multiple inputs, resulting in multiple tests (based from the same function). I talked about this previously, but used a different method then. This time, I opted to try the parametrize marker.

Conftest.py Method

The last time I wrote about pytest, I used parameterization. However, I implemented it a bit differently, using fixtures defined in the conftest.py file to do the parameterization.

For example, this fixture function loops through a list of post names, and returns the relative url for each post:

@pytest.fixture(params=POST_NAMES)
def post_url(request):
    """Returns the post urls for testing."""
    return BASE_URL + "/post/" + request.param.lower()

Next, I used that fixture as the single parameter in my test function:

def test_post_served(post_url):
    """Checks that the desired posts are available"""
    response = requests.get(post_url)
    assert response.status_code == 200

This causes the test to loop across each item the fixture returns from the POST_NAMES constant list, resulting in a test run for each one.

Using Pytest Parameterize

While that method works, tests can be parameterized without the overhead of setting up fixtures for each test data set. Instead, the parametrize pytest marker can be used.

I started by creating my set of test inputs:

example_resolution_ratios = [
    (1920, 1080, Fraction(16, 9)),
    (2560, 1440, Fraction(16, 9)),
    (3840, 2160, Fraction(16, 9)),
    (1440, 900, Fraction(8, 5)),
    (1600, 900, Fraction(16, 9)),
    (5120, 2880, Fraction(16, 9)),
    (6016, 3384, Fraction(16, 9)),
    (3440, 1440, Fraction(43, 18)),
]
(Note: I did this step in the previous method too. That's what `POST_NAMES` was)

Next, I added the @pytest.mark.parametrize marker to my test function:

# Example of parameters marker
@pytest.mark.parametrize(
    "horz_pixels,vert_pixels,expected_ratio",
    example_resolution_ratios,
    ids=[f"{pair[0]}x{pair[1]}" for pair in example_resolution_ratios],
)
def test_resolution_ratio(horz_pixels, vert_pixels, expected_ratio):
    """Tests resolution ratio with set of known calculation results."""
    assert resolution_ratio(horz_pixels, vert_pixels) == expected_ratio

While this marker looks intimidating (It’s larger than the actual test code!), it does a lot for us. It requires at least two parameters, a string containing parameter names to pass to the test ("horz_pixels,vert_pixels,expected_ratio" in this example), and the list of test items to iterate over (example_resolution_ratios). Notice, if the test items are a list of tuples, pytest can match each tuple value to a different parameter to pass to the test function.

Additional options can be provided to the parametrize marker. For example, I used the ids parameter to define how each parametrized test instance is identified when run. This test function verifies a ratio calculator, so using a list comprehension, I was able to have the resolution used for each test displayed in the run output:

...
/tests/test_calcs.py::test_resolution_ratio[1920x1080] PASSED [  3%]
/tests/test_calcs.py::test_resolution_ratio[2560x1440] PASSED [  6%]
/tests/test_calcs.py::test_resolution_ratio[3840x2160] PASSED [ 10%]
/tests/test_calcs.py::test_resolution_ratio[1440x900] PASSED [ 13%]
/tests/test_calcs.py::test_resolution_ratio[1600x900] PASSED [ 16%]
/tests/test_calcs.py::test_resolution_ratio[5120x2880] PASSED [ 20%]
/tests/test_calcs.py::test_resolution_ratio[6016x3384] PASSED [ 23%]
/tests/test_calcs.py::test_resolution_ratio[3440x1440] PASSED [ 26%]
...

This is extremely helpful when a few tests are failing, as you can easily identify what specific inputs are failing.

Conclusion

That’s really all there is to it. If you’re using pytest and want to parameterize the test functions, the parametrize marker is a simple, but powerful tool to do so. We use it often at work for some complicated setups, but I enjoyed getting to try here in a much simpler case!

Next Post:
Prev Post:

Install Podman on M1 Mac Importing ZFS Pools on Unraid