Skip to content

Typed, declarative case parametrization for pytest with async support, class-method support, shared storage, and automatic case module discovery.

License

Notifications You must be signed in to change notification settings

zerlok/pytest-case-provider

Repository files navigation

pytest-case-provider

Latest Version Python Supported Versions Pytest versions MyPy Strict Test Coverage Downloads GitHub stars

Parametrization for pytest with fixture support, async support, class-method support, shared storage, and module-level case discovery. Declarative, typed, on-demand injection for sync, async, iterable, and async-iterable providers.


Overview

Extends pytest’s parametrization layer with case storages and containers. Containers attach case sets to test functions and test methods via inject_func and inject_method. Fixtures integrate natively. Async is supported. Matching case_*.py modules are scanned and loaded. Duplicate case names collapse to single instances.


Core Concepts

Providers

Functions that yield case values. Supported forms:

  • Sync
  • Async
  • Iterable
  • Async iterable
  • Fixture-dependent

Storage

CaseStorage instances hold providers. CompositeCaseStorage merges multiple storages.

Container

CaseContainer wraps a storage and exposes:

  • .case()
  • .include()
  • .inject_func()
  • .inject_method()

A single container instance feeds any number of tests.

Injectors

inject_func() and inject_method() create isolated containers for direct use. .include() attaches external storages to them.

Module-Level Loading

If test_x.py exists alongside case_x.py, the plugin imports the case module and registers its providers. Tests using inject_func() or inject_method() receive matching providers. Matching is based on the type annotation of the case parameter.


Installation

pip install pytest-case-provider

Quick Start

# test_example.py
import typing
from dataclasses import dataclass
from pytest_case_provider import CaseContainer

@dataclass(frozen=True)
class MyCase:
    foo: int

container = CaseContainer[MyCase]()

@container.inject_func()
def test_cases(case: MyCase) -> None:
    assert isinstance(case, MyCase)

class TestGroup:
    @container.inject_method()
    def test_group(self, case: MyCase) -> None:
        assert case.foo >= 1

@container.case()
def case_small() -> MyCase:
    return MyCase(foo=1)

@container.case()
async def case_async() -> MyCase:
    return MyCase(foo=999)

@container.case()
def case_range() -> typing.Iterator[MyCase]:
    yield MyCase(foo=10)

Module-Level Case Files

tests/
    test_math.py
    case_math.py

case_math.py:

from dataclasses import dataclass

@dataclass(frozen=True)
class MathCase:
    x: int
    y: int

def case_x1_y2() -> MathCase:
    return MathCase(1, 2)

test_math.py:

from pytest_case_provider import inject_func
from tests.case_math import MathCase

@inject_func()
def test_add(case: MathCase) -> None:
    assert case.x + case.y == 3

The plugin imports case_math and binds its providers to the injector.


Pytest Expansion

test_example.py::test_cases[case_small]
test_example.py::test_cases[case_async]
test_example.py::test_cases[case_range0]
test_example.py::test_cases[case_range1]
test_example.py::TestGroup::test_group[case_small]
test_example.py::TestGroup::test_group[case_async]
test_example.py::TestGroup::test_group[case_range0]
test_example.py::TestGroup::test_group[case_range1]

Notes

  • Async requires pytest-asyncio.
  • AnyIO execution paths are disabled.
  • Case deduplication follows provider-name semantics.