84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205 | def case(name_or_gen: Union[str, Iterable[Any]], *args: Any, **kwargs: Any) -> Callable[..., Any]:
"""
Decorator to define test cases for pytest.
The `case` decorator is used to create parameterized test cases for pytest.
It lets the same test function run with different configurations while maintaining
clear test case identification.
Args:
name_or_gen (Union[str, Iterable[Any]]):
- If a string, specifies the unique identifier (ID) for the test case.
- If an iterable, enables the creation of generator-based test cases, allowing
multiple sets of parameters to be defined dynamically.
*args (Any): Positional arguments to be passed as parameters to the decorated test
function. These are mapped to the function's required arguments.
**kwargs (Any): Keyword arguments to be passed as parameters to the decorated test
function. These are matched by name with the function's parameters.
Special keyword argument `marks` can be used to specify pytest marks
(e.g., `@pytest.mark.skip`).
If provided as part of an iterable `name_or_gen` parameter, the `name`
keyword will be used to represent a template string for each generated test
case's name (e.g., "test for {}").
Returns:
Callable[..., Any]: The decorated function.
Behavior:
- If the `name_or_gen` argument is a string, the decorator creates a single test case with
the specified ID.
- If `name_or_gen` is an iterable, it generates multiple test cases dynamically, each
corresponding to a combination of the iterable values and the input arguments.
In that case (pun intended), the `name` keyword is used to represent a template string
for each test cases' name (e.g., "test {}").
Advanced Features:
1. **Marks Support**:
Use the `marks` keyword in `kwargs` to attach pytest marks to a specific
test case. Marks can be used in the same way as with
[pytest marks](https://docs.pytest.org/en/stable/reference/reference.html#marks)
2. **Existing Case Wrapping**:
When applied to an already-decorated function, the `case` decorator unwraps its
parameters and arguments, allowing additional cases or modifications without conflicts.
3. **Default Argument Handling**:
Automatically integrates function defaults with provided arguments.
Examples:
>>> import pytest
>>> from pytest_case import case
>>>
>>> @case("valid_credentials", "root", "toor")
>>> @case("invalid_credentials", "user123", "password456", marks=pytest.mark.xfail)
>>> def test_login(username: str, password: str) -> None:
>>> assert login(username, password)
Notes:
- The decorator validates the provided arguments to ensure compatibility with the
target function and raises errors for invalid or missing inputs.
- For complex cases, use the generator-based approach (first parameter as an iterable)
to simplify dynamic test creation and avoid redundant repetition.
"""
marks = kwargs.pop(MARKS_PARAM_NAME, None) or tuple()
def decorator(func: Callable[[Any], Any]) -> Callable[[Any], Any]:
# Can't check for `Iterable` because `str` is also iterable
if not isinstance(name_or_gen, str):
return _generator_case(name_or_gen, func, **kwargs)
name = name_or_gen
validate_case_inputs(func, args, kwargs)
argnames = []
defaults: Dict[str, Any] = {}
case_param_sets: List[ParameterSet] = []
if is_case(func):
# If wrapping an existing case
unwrapped_func_attrs = unwrap_func(func)
func = unwrapped_func_attrs.unwrapped_func
argnames = unwrapped_func_attrs.argnames
case_param_sets = unwrapped_func_attrs.argvalues
defaults = unwrapped_func_attrs.defaults
else:
# If wrapping a new test function
func_params = get_func_param_names(func)
defaults = get_func_optional_params(func)
func_required_params = func_params[: len(func_params) - len(defaults)][
: len(args) + len(kwargs)
]
# All the rest should be fixtures or will raise an error because tey are not provided
argnames = (*func_required_params, *defaults.keys())
func.__defaults__ = None
case_argvalues = []
for arg_index, argname in enumerate(argnames):
new_argvalue = None
if argname in defaults:
# Has default value - use it
new_argvalue = defaults[argname]
if arg_index < len(args):
# Provided in args - use it
new_argvalue = args[arg_index]
if argname in kwargs:
# Provided in kwargs - use it
new_argvalue = kwargs[argname]
case_argvalues.append(new_argvalue)
case_param_sets = [pytest.param(*case_argvalues, marks=marks, id=name)] + case_param_sets
return wrap_func(
argnames=argnames,
param_sets=case_param_sets,
defaults=defaults,
)(func)
return decorator
|