Parametrization¶
Parametrization allows you to run the same test with different input values, reducing code duplication and making your tests more comprehensive.
Basic Parametrization¶
Use the @parametrize decorator to run a test multiple times with different arguments:
from rustest import parametrize
@parametrize("input,expected", [
(1, 2),
(2, 4),
(3, 6),
])
def test_double(input: int, expected: int) -> None:
assert input * 2 == expected
This creates three separate test cases:
PASSED 0.001s test_double[case_0]
PASSED 0.001s test_double[case_1]
PASSED 0.001s test_double[case_2]
Parameter Formats¶
Comma-Separated String¶
You can specify parameter names as a comma-separated string:
from rustest import parametrize
@parametrize("x,y,expected", [
(1, 1, 2),
(2, 3, 5),
(10, 5, 15),
])
def test_addition(x: int, y: int, expected: int) -> None:
assert x + y == expected
List of Strings¶
Or as a list of strings:
from rustest import parametrize
@parametrize(["x", "y", "expected"], [
(1, 1, 2),
(2, 3, 5),
(10, 5, 15),
])
def test_addition(x: int, y: int, expected: int) -> None:
assert x + y == expected
Single Parameter¶
For a single parameter, pass values directly:
from rustest import parametrize
@parametrize("value", [1, 2, 3, 4, 5])
def test_is_positive(value: int) -> None:
assert value > 0
Or as tuples if you prefer consistency:
from rustest import parametrize
@parametrize("value", [(1,), (2,), (3,)])
def test_is_positive(value: int) -> None:
assert value > 0
Custom Test IDs¶
Provide custom IDs to make test output more readable:
from rustest import parametrize
@parametrize("value,expected", [
(2, 4),
(3, 9),
(4, 16),
], ids=["two", "three", "four"])
def test_square(value: int, expected: int) -> None:
assert value ** 2 == expected
Output:
Descriptive IDs¶
Use descriptive IDs for complex test cases:
from rustest import parametrize
@parametrize("operation,a,b,expected", [
("add", 2, 3, 5),
("subtract", 5, 3, 2),
("multiply", 4, 3, 12),
("divide", 10, 2, 5),
], ids=["addition", "subtraction", "multiplication", "division"])
def test_calculator(operation: str, a: int, b: int, expected: int) -> None:
if operation == "add":
assert a + b == expected
elif operation == "subtract":
assert a - b == expected
elif operation == "multiply":
assert a * b == expected
elif operation == "divide":
assert a / b == expected
Parametrizing with Fixtures¶
Combine parametrized tests with fixtures:
from rustest import fixture, parametrize
@fixture
def multiplier() -> int:
return 10
@parametrize("value,expected", [
(1, 10),
(2, 20),
(3, 30),
])
def test_multiply(multiplier: int, value: int, expected: int) -> None:
assert multiplier * value == expected
Indirect Parametrization¶
The indirect parameter allows you to use fixture references in parametrization. When a parameter is marked as indirect, its value is treated as a fixture name, and that fixture is resolved:
Using indirect with a List¶
Specify which parameters should be resolved as fixtures:
from rustest import fixture, parametrize
@fixture
def data_1():
return {"value": 42, "name": "first"}
@fixture
def data_2():
return {"value": 100, "name": "second"}
@parametrize("data_fixture, multiplier", [
("data_1", 2),
("data_2", 3),
], indirect=["data_fixture"])
def test_with_indirect(data_fixture: dict, multiplier: int) -> None:
# data_fixture is resolved as a fixture
# multiplier is used as a direct value
result = data_fixture["value"] * multiplier
assert result in [84, 300] # 42*2 or 100*3
Using indirect=True¶
Mark all parameters as indirect:
from rustest import fixture, parametrize
@fixture
def dataset_a():
return [1, 2, 3]
@fixture
def dataset_b():
return [4, 5, 6]
@parametrize("data", ["dataset_a", "dataset_b"], indirect=True)
def test_all_positive(data: list) -> None:
# Both 'dataset_a' and 'dataset_b' strings are resolved as fixtures
assert all(x > 0 for x in data)
Single Parameter as Indirect¶
Use a string to mark one parameter:
from rustest import fixture, parametrize
@fixture
def config_dev():
return {"env": "dev", "debug": True}
@fixture
def config_prod():
return {"env": "prod", "debug": False}
@parametrize("config, expected_env", [
("config_dev", "dev"),
("config_prod", "prod"),
], indirect="config")
def test_environment(config: dict, expected_env: str) -> None:
assert config["env"] == expected_env
Why Use Indirect Parametrization?¶
Indirect parametrization is the standard pytest pattern for parametrizing with fixtures. It's useful when:
- Testing with different configurations: Use different fixture instances for each test case
- Complex setup per parameter: Each fixture can have its own setup/teardown logic
- Fixture reuse: Same fixtures used in parametrization can be used directly in other tests
- Type safety: IDE autocomplete works with fixture names
This replaces the need for third-party plugins like pytest-lazy-fixtures.
Complex Parameter Values¶
Using Dictionaries¶
Pass dictionaries as parameter values:
from rustest import parametrize
@parametrize("user", [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35},
], ids=["alice", "bob", "charlie"])
def test_user_valid(user: dict) -> None:
assert "name" in user
assert user["age"] > 0
Using Objects¶
from dataclasses import dataclass
from rustest import parametrize
@dataclass
class User:
name: str
email: str
@parametrize("user", [
User("Alice", "alice@example.com"),
User("Bob", "bob@example.com"),
], ids=["alice", "bob"])
def test_user_email(user: User) -> None:
assert "@" in user.email
Using Lists¶
from rustest import parametrize
@parametrize("numbers", [
[1, 2, 3],
[10, 20, 30],
[100, 200, 300],
])
def test_sum_positive(numbers: list) -> None:
assert sum(numbers) > 0
Multiple Parametrize Decorators¶
You can stack @parametrize decorators to test all combinations:
from rustest import parametrize
@parametrize("x", [1, 2])
@parametrize("y", [3, 4])
def test_combinations(x: int, y: int) -> None:
assert x < y
This creates 4 test cases:
- test_combinations[case_0-case_0] (x=1, y=3)
- test_combinations[case_0-case_1] (x=1, y=4)
- test_combinations[case_1-case_0] (x=2, y=3)
- test_combinations[case_1-case_1] (x=2, y=4)
Parametrizing Test Classes¶
Apply parametrization to all methods in a test class:
from rustest import parametrize
@parametrize("value", [1, 2, 3])
class TestNumber:
def test_positive(self, value: int) -> None:
assert value > 0
def test_less_than_ten(self, value: int) -> None:
assert value < 10
This runs both tests for each value (6 total tests).
Real-World Examples¶
Testing Edge Cases¶
from rustest import parametrize
@parametrize("text,expected", [
("", 0), # Empty string
("a", 1), # Single character
("hello", 5), # Normal case
("hello world", 11), # With space
("🎉", 1), # Unicode emoji
], ids=["empty", "single", "normal", "with_space", "emoji"])
def test_string_length(text: str, expected: int) -> None:
assert len(text) == expected
Testing Multiple Data Types¶
from rustest import parametrize
@parametrize("value,expected_type", [
(42, int),
(3.14, float),
("hello", str),
([1, 2, 3], list),
({"key": "value"}, dict),
], ids=["int", "float", "str", "list", "dict"])
def test_type_checking(value, expected_type):
assert isinstance(value, expected_type)
Testing Error Conditions¶
from rustest import parametrize, raises
@parametrize("invalid_input,error_type", [
("abc", ValueError),
("", ValueError),
(None, TypeError),
], ids=["non_numeric", "empty", "none"])
def test_invalid_conversion(invalid_input, error_type):
with raises(error_type):
int(invalid_input)
Testing API Responses¶
from rustest import parametrize
class MockResponse:
def __init__(self, status_code):
self.status_code = status_code
class MockAPIClient:
def get(self, endpoint):
if endpoint.startswith("/api/") and endpoint != "/api/invalid":
return MockResponse(200)
return MockResponse(404)
@parametrize("endpoint,expected_status", [
("/api/users", 200),
("/api/posts", 200),
("/api/invalid", 404),
], ids=["users", "posts", "not_found"])
def test_api_endpoints(endpoint: str, expected_status: int):
api_client = MockAPIClient()
response = api_client.get(endpoint)
assert response.status_code == expected_status
Best Practices¶
Use Meaningful IDs¶
from rustest import parametrize
def is_adult(age: int) -> bool:
return age >= 18
# Good - clear what's being tested
@parametrize("age,valid", [
(17, False),
(18, True),
(65, True),
], ids=["underage", "adult", "senior"])
def test_age_validation(age: int, valid: bool):
assert is_adult(age) == valid
# Less clear
@parametrize("age,valid", [
(17, False),
(18, True),
(65, True),
])
def test_age_validation(age: int, valid: bool):
assert is_adult(age) == valid
Keep Test Cases Focused¶
from rustest import parametrize
# Good - focused test cases
@parametrize("value", [1, 2, 3, 100, 1000])
def test_positive_numbers(value: int):
assert value > 0
@parametrize("value", [-1, -10, -100])
def test_negative_numbers(value: int):
assert value < 0
# Less ideal - mixing concerns
@parametrize("value,expected", [
(1, "positive"),
(-1, "negative"),
(100, "positive"),
(-100, "negative"),
])
def test_number_sign(value: int, expected: str):
# Test logic becomes complex
if expected == "positive":
assert value > 0
else:
assert value < 0
Document Complex Parameters¶
from rustest import parametrize
class ConfigResult:
def __init__(self, cache_status: str):
self.cache_status = cache_status
def run_with_config(config: dict) -> ConfigResult:
if config.get("mock"):
return ConfigResult("mocked")
elif config.get("cache"):
return ConfigResult("cached")
else:
return ConfigResult("uncached")
@parametrize("config,expected_result", [
# Production config with caching enabled
({"env": "prod", "cache": True}, "cached"),
# Development config without caching
({"env": "dev", "cache": False}, "uncached"),
# Test config with mock cache
({"env": "test", "cache": True, "mock": True}, "mocked"),
], ids=["production", "development", "testing"])
def test_environment_behavior(config: dict, expected_result: str):
result = run_with_config(config)
assert result.cache_status == expected_result
Next Steps¶
- Fixtures - Combine fixtures with parametrization
- Marks & Skipping - Mark parametrized tests
- Test Classes - Parametrize test classes