Case parametrization for pytest with fixture, class, and async support.
Provides declarative, typed, on demand case injection for both sync and async test functions.
pytest-case-provider extends pytest’s parametrization system.
It was inspired by pytest-cases, but redesigned from scratch for strict typing, async support, and fixture-native case injection.
It allows attaching case providers directly to test functions or methods via @inject_cases_func and
@inject_cases_method.
Cases can be:
- Synchronous or asynchronous.
- Iterable or async-iterable.
- Fixture-dependent.
- Composable across tests or classes.
| Symbol | Description |
|---|---|
inject_cases_func |
Decorator/injector for test functions |
inject_cases_method |
Same as above, for test class methods |
CaseStorage[T] |
Mutable case storage container |
CompositeCaseStorage[T] |
Aggregates multiple CaseCollector |
pip install pytest-case-providerimport typing
from dataclasses import dataclass, replace
import pytest
from pytest_case_provider import inject_cases_func, inject_cases_method
@dataclass(frozen=True)
class MyCase:
foo: int
@pytest.fixture
def number() -> int:
return 42
# Regular test
def test_without_case_injection() -> None:
assert True
# Case-enabled test function
@inject_cases_func()
def test_case_injected(case: MyCase) -> None:
assert isinstance(case, MyCase)
# Cross-inject from another test
@inject_cases_func.include(test_case_injected)
def test_case_increment(case: MyCase, case_foo_inc: MyCase) -> None:
assert case.foo + 1 == case_foo_inc.foo
# Define case providers
@test_case_injected.case()
def case_one() -> MyCase:
return MyCase(foo=1)
@test_case_injected.case()
def case_two() -> MyCase:
return MyCase(foo=2)
# Use other fixtures in case providers
@test_case_injected.case()
def case_number(number: int) -> MyCase:
return MyCase(foo=number)
# Async case provider
@test_case_injected.case()
async def case_async_generated() -> MyCase:
return MyCase(foo=999)
# Iterable provider
@test_case_injected.case()
def case_iterable() -> typing.Iterator[MyCase]:
yield MyCase(foo=10)
# Async iterable provider
@test_case_injected.case()
async def case_async_iterable() -> typing.Iterator[MyCase]:
yield MyCase(foo=20)
# Access case object from fixture
@pytest.fixture
def case_foo_inc(case: MyCase) -> MyCase:
return replace(case, foo=case.foo + 1)
# Example class-based usage
class TestClass:
@inject_cases_method()
def test_class_cases(self, case: MyCase) -> None:
assert isinstance(case, MyCase)
@test_class_cases.case()
def case_three(self) -> MyCase:
return MyCase(foo=3)
@test_class_cases.case()
def case_four(self) -> MyCase:
return MyCase(foo=4)Pytest will expand each injected case as a distinct test variant using pytest's parametrization:
test_example.py::test_case_injected[case_one]
test_example.py::test_case_injected[case_two]
test_example.py::test_case_injected[case_number]
test_example.py::test_case_injected[case_async_generated]
test_example.py::test_case_injected[case_iterable]
test_example.py::test_case_injected[case_async_iterable]
test_example.py::test_case_increment[case_one]
test_example.py::test_case_increment[case_two]
test_example.py::test_case_increment[case_number]
test_example.py::test_case_increment[case_async_generated]
test_example.py::test_case_increment[case_iterable]
test_example.py::test_case_increment[case_async_iterable]
test_example.py::TestClass::test_class_cases[case_three]
test_example.py::TestClass::test_class_cases[case_four]