Pytest Tutorial - 8 | Advanced - Parametrization Techniques
In this article, we will explore advanced techniques to master parametrization in Pytest, enabling you to write more scalable and flexible test cases. Pytest's @pytest.mark.parametrize
is a powerful feature that allows us to run the same test multiple times with different data inputs. However, beyond the basics, there are more sophisticated ways to leverage this tool, which we’ll cover here.
Recap: Basic Parametrization
If you're already familiar with basic parametrization in Pytest, you'll know that it involves the @pytest.mark.parametrize
decorator, which allows passing various inputs to a single test function. Here’s a quick recap of a basic example:
import pytest
@pytest.mark.parametrize("a, b, expected", [(1, 2, 3), (4, 5, 9), (10, -5, 5)])
def test_addition(a, b, expected):
assert a + b == expected
In this simple example, the test runs three times, each with a different set of input values for a
, b
, and expected
.
1. Parametrizing Multiple Arguments
Often, you’ll want to test functions with multiple parameters. Parametrization can handle multiple sets of arguments at once, making it possible to test combinations of inputs.
Simple Example: Testing Arithmetic Operations
import pytest
@pytest.mark.parametrize("a, b", [(1, 2), (3, 4)])
@pytest.mark.parametrize("operation", ["add", "multiply"])
def test_operations(a, b, operation):
if operation == "add":
assert a + b == a + b
elif operation == "multiply":
assert a * b == a * b
Here, Pytest combines both sets of arguments, running the test four times:
a=1
,b=2
,operation="add"
a=1
,b=2
,operation="multiply"
a=3
,b=4
,operation="add"
a=3
,b=4
,operation="multiply"
This technique is particularly useful when testing combinations of multiple features or behaviors in your code.
Complex Example: Testing API Responses with Multiple Endpoints and Methods
import pytest
import requests
endpoints = [
("/users", "GET"),
("/users", "POST"),
("/posts", "GET"),
("/posts", "PUT")
]
@pytest.mark.parametrize("endpoint, method", endpoints)
def test_api_requests(endpoint, method):
base_url = "https://jsonplaceholder.typicode.com"
if method == "GET":
response = requests.get(base_url + endpoint)
assert response.status_code == 200
elif method == "POST":
response = requests.post(base_url + endpoint, json={"title": "test"})
assert response.status_code == 201
elif method == "PUT":
response = requests.put(base_url + endpoint + "/1", json={"title": "updated"})
assert response.status_code == 200
This test case runs multiple requests against different API endpoints and methods (GET, POST, PUT). Pytest will run this test for all possible combinations of endpoint and method.
2. Parametrization with Fixtures
You can combine @pytest.mark.parametrize
with fixtures to create even more dynamic and powerful test scenarios. Fixtures allow you to set up environments or objects required for testing, while parametrization allows you to test those environments with different inputs.
Simple Example: Parametrizing Test Data with a Fixture
import pytest
@pytest.fixture
def setup_data():
return {"base": 10, "multiplier": 2}
@pytest.mark.parametrize("increment", [1, 5, 10])
def test_calculate(setup_data, increment):
result = setup_data["base"] * setup_data["multiplier"] + increment
assert result in [21, 25, 30]
In this example, the fixture setup_data
provides a base setup for each test, while the test function uses different increments to calculate the result. Combining fixtures and parametrization lets you test the same scenario in multiple configurations.
Complex Example: Parametrizing with a Database Fixture
import pytest
import sqlite3
# Fixture to set up a database connection
@pytest.fixture
def db_connection():
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INT, name TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'John'), (2, 'Jane')")
yield conn
conn.close()
# Parametrize SQL queries and expected results
@pytest.mark.parametrize("query, expected", [
("SELECT name FROM users WHERE id = 1", "John"),
("SELECT name FROM users WHERE id = 2", "Jane")
])
def test_database_queries(db_connection, query, expected):
cursor = db_connection.execute(query)
result = cursor.fetchone()[0]
assert result == expected
This example sets up an in-memory SQLite database and parametrizes different SQL queries to ensure that the correct results are returned for each query. The combination of fixtures and parametrization allows the database setup to be shared across tests.
3. Using Indirect Parametrization
Sometimes, you want to parametrize not just the inputs but also parts of the test setup itself. You can achieve this using indirect parametrization, where the arguments are used to indirectly control the fixture or setup.
Simple Example: Indirect Parametrization of URLs
import pytest
@pytest.fixture
def url_builder(request):
base_url = "https://example.com"
return f"{base_url}/{request.param}"
@pytest.mark.parametrize("url_builder", ["home", "about", "contact"], indirect=True)
def test_indirect_urls(url_builder):
assert "https://example.com/" in url_builder
Here, the fixture url_builder
dynamically builds a URL based on the parameter values passed in pytest.mark.parametrize
. This makes the test reusable for different pages.
Complex Example: Configuring a Mock Service with Indirect Parametrization
import pytest
@pytest.fixture
def mock_service(request):
class MockService:
def __init__(self, mode):
self.mode = mode
def respond(self):
if self.mode == "normal":
return "Service Running"
elif self.mode == "error":
return "Service Error"
return MockService(request.param)
# Indirect parametrization controlling the mock service behavior
@pytest.mark.parametrize("mock_service", ["normal", "error"], indirect=True)
def test_service_response(mock_service):
response = mock_service.respond()
assert response in ["Service Running", "Service Error"]
The test parametrizes the mock_service
fixture indirectly, setting its mode to either "normal" or "error". This allows you to test how the service behaves in different scenarios.
4. Parametrizing Complex Data Structures
You can also parametrize over complex data structures such as dictionaries, lists, or even custom objects. This is particularly useful when you need to pass multiple related pieces of information into your tests.
Simple Example: Parametrizing with Dictionaries
import pytest
test_data = [
{"input": {"a": 1, "b": 2}, "expected": 3},
{"input": {"a": 10, "b": 5}, "expected": 15},
{"input": {"a": -1, "b": 1}, "expected": 0}
]
@pytest.mark.parametrize("data", test_data)
def test_dict_param(data):
result = data["input"]["a"] + data["input"]["b"]
assert result == data["expected"]
In this test, each element of the test_data
list is a dictionary. The test then accesses the dictionary keys (input
and expected
) to verify the results. This technique makes tests more readable and maintainable when dealing with complex inputs.
Complex Example: Parametrizing with Nested Dictionaries
import pytest
test_data = [
{"user": {"name": "Alice", "age": 25}, "account": {"status": "active", "balance": 1000}},
{"user": {"name": "Bob", "age": 30}, "account": {"status": "inactive", "balance": 0}}
]
@pytest.mark.parametrize("data", test_data)
def test_user_account_status(data):
if data["account"]["status"] == "active":
assert data["user"]["age"] < 60
else:
assert data["account"]["balance"] == 0
This example demonstrates how you can pass deeply nested dictionaries as parameters. It checks if active users are below a certain age and ensures that inactive accounts have a zero balance.
Conclusion
Advanced parametrization techniques in Pytest help you scale your test coverage by combining multiple sets of inputs, fixtures, and conditions. With these techniques, you can write more flexible and powerful tests that are not only easy to maintain but also cover a wide range of scenarios.
By mastering the techniques covered in this article—such as multi-parameter tests, indirect parametrization, handling complex data structures, and dynamic data loading—you can take your testing skills to the next level.
Exercises
- Try combining three or more sets of parameters using
@pytest.mark.parametrize
and observe how Pytest handles the Cartesian product of your inputs. - Experiment with dynamically loading test data from JSON files or even APIs.
- Explore ways to conditionally skip parametrized tests based on environment variables or external factors.
With these techniques, your tests will be more flexible and maintainable as your codebase grows. Happy testing!
Comments
Post a Comment