DEV Community

Kelvin Wangonya
Kelvin Wangonya

Posted on • Originally published at wangonya.com

7 1

Writing DRYer tests using Pytest parametrize

Tests tend to not always be so DRY, which isn't necessarily a bad thing.

This SO answer sums it up nicely:

Readability is more important for tests. If a test fails, you want the problem to be obvious. The developer shouldn't have to wade through a lot of heavily factored test code to determine exactly what failed. You don't want your test code to become so complex that you need to write unit-test-tests.

However, eliminating duplication is usually a good thing, as long as it doesn't obscure anything, and eliminating the duplication in your tests may lead to a better API. Just make sure you don't go past the point of diminishing returns.

Pytest gives some ways to reduce duplication with fixtures.

Say you had a couple of endpoints that return data to be used in a report.
The report data is supposed to be displayed in an excel sheet with different sheets.

Sometimes, only data for one sheet is required. Other times, data for all the sheets is fetched. So the endpoints end up being broken down like this:

/report/sheet-a
/report/sheet-b
/report/sheet-c
/report/sheet-d
Enter fullscreen mode Exit fullscreen mode

Let's take a simple test case: checking that the endpoints return 200 when called.

import pytest

def test_report_sheet_a_returns_200():
    response = client.get('/report/sheet-a')
    assert response.status == 200

def test_report_sheet_b_returns_200():
    response = client.get('/report/sheet-b')
    assert response.status == 200

def test_report_sheet_c_returns_200():
    response = client.get('/report/sheet-c')
    assert response.status == 200

def test_report_sheet_d_returns_200():
    response = client.get('/report/sheet-d')
    assert response.status == 200
Enter fullscreen mode Exit fullscreen mode

This might not look too bad, but if we wanted to test for another operation on the endpoints - checking if the endpoints require authentication for example - you get the feeling that this can be done a little bit better.

import pytest

# test GET requests

def test_report_sheet_a_returns_200():
    response = client.get('/report/sheet-a')
    assert response.status == 200

def test_report_sheet_b_returns_200():
    response = client.get('/report/sheet-b')
    assert response.status == 200

def test_report_sheet_c_returns_200():
    response = client.get('/report/sheet-c')
    assert response.status == 200

def test_report_sheet_d_returns_200():
    response = client.get('/report/sheet-d')
    assert response.status == 200

# test auth

def test_report_sheet_a_requires_auth():
    response = unauthorized_client.get('/report/sheet-a')
    assert response.status == 401

def test_report_sheet_b_requires_auth():
    response = unauthorized_client.get('/report/sheet-b')
    assert response.status == 401

def test_report_sheet_c_requires_auth():
    response = unauthorized_client.get('/report/sheet-c')
    assert response.status == 401

def test_report_sheet_d_requires_auth():
    response = unauthorized_client.get('/report/sheet-d')
    assert response.status == 401
Enter fullscreen mode Exit fullscreen mode

Notice that the only thing changing in the tests is the endpoints. Everything else remains the same.

Given that all the sheets belong to one report, we can refactor the tests to reduce duplication without sacrificing readability.

Here's how the tests can be rewritten using Pytest parametrize:

import pytest

report_sheet_endpoints = (
  '/sheet-a',
  '/sheet-b',
  '/sheet-c',
  '/sheet-d',
)


@pytest.mark.parametrize('endpoint', report_sheet_endpoints)
def test_report_sheets_return_200(endpoint)
    response = client.get(f'/report{endpoint}')
    assert response.status == 200


@pytest.mark.parametrize('endpoint', report_sheet_endpoints)
def test_report_sheets_require_auth(endpoint)
    response = unauthorized_client.get(f'/report{endpoint}')
    assert response.status == 401
Enter fullscreen mode Exit fullscreen mode

Now we have two tests instead of eight. But when you run the tests, 8 tests will run, not 2. Pytest takes each value in report_sheet_endpoints and feeds it into the test.
This reduces duplication while maintaining readability.

Running the tests gives this output:

test_reports.py::test_report_sheets_return_200[sheet-a] PASSED
test_reports.py::test_report_sheets_return_200[sheet-b] PASSED
test_reports.py::test_report_sheets_return_200[sheet-c] PASSED
test_reports.py::test_report_sheets_return_200[sheet-d] PASSED

test_reports.py::test_report_sheets_require_auth[sheet-a] PASSED
test_reports.py::test_report_sheets_require_auth[sheet-b] PASSED
test_reports.py::test_report_sheets_require_auth[sheet-c] PASSED
test_reports.py::test_report_sheets_require_auth[sheet-d] PASSED
Enter fullscreen mode Exit fullscreen mode

Warp.dev image

The best coding agent. Backed by benchmarks.

Warp outperforms every other coding agent on the market, and gives you full control over which model you use. Get started now for free, or upgrade and unlock 2.5x AI credits on Warp's paid plans.

Download Warp

Top comments (1)

Collapse
 
erikwhiting88 profile image
Erik

I had no idea you could do this, I have so many test suites I can update with this information

Feature flag article image

Create a feature flag in your IDE in 5 minutes with LaunchDarkly’s MCP server 🏁

How to create, evaluate, and modify flags from within your IDE or AI client using natural language with LaunchDarkly's new MCP server. Follow along with this tutorial for step by step instructions.

Read full post

👋 Kindness is contagious

Discover fresh viewpoints in this insightful post, supported by our vibrant DEV Community. Every developer’s experience matters—add your thoughts and help us grow together.

A simple “thank you” can uplift the author and spark new discussions—leave yours below!

On DEV, knowledge-sharing connects us and drives innovation. Found this useful? A quick note of appreciation makes a real impact.

Okay