unittest-parametrize 1.5.0__tar.gz → 1.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,29 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ 1.7.0 (2025-08-27)
6
+ ------------------
7
+
8
+ * Add support for the ``ids`` argument being a callable.
9
+ The callable is called once per parameter value and can return a string for that value or ``None`` to use the default.
10
+
11
+ `PR #143 <https://github.com/adamchainz/unittest-parametrize/pull/143>`__.
12
+
13
+ * Fail for collisions between autogenerated IDs and user-specified IDs when mixing tuples and ``param`` instances.
14
+
15
+ `PR #143 <https://github.com/adamchainz/unittest-parametrize/pull/143>`__.
16
+
17
+ * Update the type hints to allow mixing tuples and ``param`` instances.
18
+
19
+ `PR #143 <https://github.com/adamchainz/unittest-parametrize/pull/143>`__.
20
+
21
+ 1.6.0 (2025-01-06)
22
+ ------------------
23
+
24
+ * Add support for asynchronous tests.
25
+
26
+ Thanks to Adrien Cossa in `PR #121 <https://github.com/adamchainz/unittest-parametrize/pull/121>`__.
27
+
5
28
  1.5.0 (2024-10-08)
6
29
  ------------------
7
30
 
@@ -1,15 +1,15 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: unittest-parametrize
3
- Version: 1.5.0
3
+ Version: 1.7.0
4
4
  Summary: Parametrize tests within unittest TestCases.
5
5
  Author-email: Adam Johnson <me@adamj.eu>
6
+ License-Expression: MIT
6
7
  Project-URL: Changelog, https://github.com/adamchainz/unittest-parametrize/blob/main/CHANGELOG.rst
7
8
  Project-URL: Funding, https://adamj.eu/books/
8
9
  Project-URL: Repository, https://github.com/adamchainz/unittest-parametrize
9
10
  Keywords: unittest
10
11
  Classifier: Development Status :: 5 - Production/Stable
11
12
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Natural Language :: English
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Classifier: Programming Language :: Python :: 3.9
@@ -22,6 +22,7 @@ Requires-Python: >=3.9
22
22
  Description-Content-Type: text/x-rst
23
23
  License-File: LICENSE
24
24
  Requires-Dist: typing-extensions; python_version < "3.10"
25
+ Dynamic: license-file
25
26
 
26
27
  ====================
27
28
  unittest-parametrize
@@ -73,7 +74,7 @@ __ https://docs.pytest.org/en/stable/how-to/parametrize.html#parametrize-basics
73
74
  There are two steps to parametrize a test case:
74
75
 
75
76
  1. Use ``ParametrizedTestCase`` in the base classes for your test case.
76
- 2. Apply ``@parametrize`` to any tests for parametrization.
77
+ 2. Apply ``@parametrize`` to any test methods for parametrization.
77
78
  This decorator takes (at least):
78
79
 
79
80
  * the argument names to parametrize, as comma-separated string
@@ -83,8 +84,7 @@ Here’s a basic example:
83
84
 
84
85
  .. code-block:: python
85
86
 
86
- from unittest_parametrize import parametrize
87
- from unittest_parametrize import ParametrizedTestCase
87
+ from unittest_parametrize import ParametrizedTestCase, parametrize
88
88
 
89
89
 
90
90
  class SquareTests(ParametrizedTestCase):
@@ -101,6 +101,7 @@ Here’s a basic example:
101
101
  ``@parametrize`` modifies the class at definition time with Python’s |__init_subclass__ hook|__.
102
102
  It removes the original test method and creates wrapped copies with individual names.
103
103
  Thus the parametrization should work regardless of the test runner you use (be it unittest, Django’s test runner, pytest, etc.).
104
+ It supports both synchronous and asynchronous test methods.
104
105
 
105
106
  .. |__init_subclass__ hook| replace:: ``__init_subclass__`` hook
106
107
  __ https://docs.python.org/3/reference/datamodel.html#object.__init_subclass__
@@ -112,8 +113,7 @@ You can provide argument names as a sequence of strings instead:
112
113
 
113
114
  .. code-block:: python
114
115
 
115
- from unittest_parametrize import parametrize
116
- from unittest_parametrize import ParametrizedTestCase
116
+ from unittest_parametrize import ParametrizedTestCase, parametrize
117
117
 
118
118
 
119
119
  class SquareTests(ParametrizedTestCase):
@@ -176,13 +176,20 @@ You can see these names when running the tests:
176
176
 
177
177
  OK
178
178
 
179
- You can customize these names by passing ``param`` objects, which contain the arguments and an optional ID for the suffix:
179
+ You can customize these names in several ways:
180
+
181
+ 1. Using ``param`` objects with IDs.
182
+ 2. Passing a sequence of strings as the ``ids`` argument.
183
+ 3. Passing a callable as the ``ids`` argument.
184
+
185
+ Passing ``param`` objects with IDs
186
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
187
+
188
+ Pass a ``param`` object for each parameter set, setting the test ID suffix with the optional ``id`` argument:
180
189
 
181
190
  .. code-block:: python
182
191
 
183
- from unittest_parametrize import param
184
- from unittest_parametrize import parametrize
185
- from unittest_parametrize import ParametrizedTestCase
192
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
186
193
 
187
194
 
188
195
  class SquareTests(ParametrizedTestCase):
@@ -196,7 +203,7 @@ You can customize these names by passing ``param`` objects, which contain the ar
196
203
  def test_square(self, x: int, expected: int) -> None:
197
204
  self.assertEqual(x**2, expected)
198
205
 
199
- Yielding perhaps more natural names:
206
+ Yielding more natural names:
200
207
 
201
208
  .. code-block:: console
202
209
 
@@ -215,9 +222,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
215
222
 
216
223
  .. code-block:: python
217
224
 
218
- from unittest_parametrize import param
219
- from unittest_parametrize import parametrize
220
- from unittest_parametrize import ParametrizedTestCase
225
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
221
226
 
222
227
 
223
228
  class SquareTests(ParametrizedTestCase):
@@ -231,7 +236,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
231
236
  def test_square(self, x: int, expected: int) -> None:
232
237
  self.assertEqual(x**2, expected)
233
238
 
234
- ID-free ``param``\s fall back to the default index suffixes:
239
+ The ID-free ``param``\s fall back to the default index suffixes:
235
240
 
236
241
  .. code-block:: console
237
242
 
@@ -244,12 +249,14 @@ ID-free ``param``\s fall back to the default index suffixes:
244
249
 
245
250
  OK
246
251
 
247
- Alternatively, you can provide the id’s separately with the ``ids`` argument:
252
+ Passing a sequence of strings as the ``ids`` argument
253
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
254
+
255
+ Another option is to provide the IDs in the separate ``ids`` argument:
248
256
 
249
257
  .. code-block:: python
250
258
 
251
- from unittest_parametrize import parametrize
252
- from unittest_parametrize import ParametrizedTestCase
259
+ from unittest_parametrize import ParametrizedTestCase, parametrize
253
260
 
254
261
 
255
262
  class SquareTests(ParametrizedTestCase):
@@ -264,6 +271,64 @@ Alternatively, you can provide the id’s separately with the ``ids`` argument:
264
271
  def test_square(self, x: int, expected: int) -> None:
265
272
  self.assertEqual(x**2, expected)
266
273
 
274
+ This option sets the full suffixes to the provided strings:
275
+
276
+ .. code-block:: console
277
+
278
+ $ python -m unittest t.py -v
279
+ test_square_one (example.SquareTests.test_square_one) ... ok
280
+ test_square_two (example.SquareTests.test_square_two) ... ok
281
+
282
+ ----------------------------------------------------------------------
283
+ Ran 2 tests in 0.000s
284
+
285
+ OK
286
+
287
+ Passing a callable as the ``ids`` argument
288
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
289
+
290
+ The ``ids`` argument can also be a callable, which unittest-parametrize calls once per parameter value.
291
+ The callable can return a string for that value, or ``None`` to use the default index suffix.
292
+ The values are then joined with underscores to form the full suffix.
293
+
294
+ For example:
295
+
296
+ .. code-block:: python
297
+
298
+ from unittest_parametrize import ParametrizedTestCase, parametrize
299
+
300
+
301
+ def make_id(value):
302
+ if isinstance(value, int):
303
+ return f"num{value}"
304
+ return None
305
+
306
+
307
+ class SquareTests(ParametrizedTestCase):
308
+ @parametrize(
309
+ "x,expected",
310
+ [
311
+ (1, 1),
312
+ (2, 4),
313
+ ],
314
+ ids=make_id,
315
+ )
316
+ def test_square(self, x: int, expected: int) -> None:
317
+ self.assertEqual(x**2, expected)
318
+
319
+ …yields:
320
+
321
+ .. code-block:: console
322
+
323
+ $ python -m unittest t.py -v
324
+ test_square_num1_num1 (example.SquareTests.test_square_num1_num1) ... ok
325
+ test_square_num2_num4 (example.SquareTests.test_square_num2_num4) ... ok
326
+
327
+ ----------------------------------------------------------------------
328
+ Ran 2 tests in 0.000s
329
+
330
+ OK
331
+
267
332
  Use with other test decorators
268
333
  ------------------------------
269
334
 
@@ -274,8 +339,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
274
339
  .. code-block:: python
275
340
 
276
341
  from unittest import mock
277
- from unittest_parametrize import parametrize
278
- from unittest_parametrize import ParametrizedTestCase
342
+ from unittest_parametrize import ParametrizedTestCase, parametrize
279
343
 
280
344
 
281
345
  class CarpentryTests(ParametrizedTestCase):
@@ -284,8 +348,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
284
348
  [(11,), (17,)],
285
349
  )
286
350
  @mock.patch("example.hammer", autospec=True)
287
- def test_nail_a_board(self, mock_hammer, nails):
288
- ...
351
+ def test_nail_a_board(self, mock_hammer, nails): ...
289
352
 
290
353
  Also note that due to how ``mock.patch`` always adds positional arguments at the start, the parametrized arguments must come last.
291
354
  ``@parametrize`` always adds parameters as keyword arguments, so you can also use `keyword-only syntax <https://peps.python.org/pep-3102/>`__ for parametrized arguments:
@@ -293,8 +356,7 @@ Also note that due to how ``mock.patch`` always adds positional arguments at the
293
356
  .. code-block:: python
294
357
 
295
358
  # ...
296
- def test_nail_a_board(self, mock_hammer, *, nails):
297
- ...
359
+ def test_nail_a_board(self, mock_hammer, *, nails): ...
298
360
 
299
361
  Multiple ``@parametrize`` decorators
300
362
  ------------------------------------
@@ -304,8 +366,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
304
366
 
305
367
  .. code-block:: python
306
368
 
307
- from unittest_parametrize import parametrize
308
- from unittest_parametrize import ParametrizedTestCase
369
+ from unittest_parametrize import ParametrizedTestCase, parametrize
309
370
 
310
371
 
311
372
  class RocketTests(ParametrizedTestCase):
@@ -317,8 +378,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
317
378
  for hyperdrive_level in [0, 1, 2]
318
379
  ],
319
380
  )
320
- def test_takeoff(self, use_ions, hyperdrive_level) -> None:
321
- ...
381
+ def test_takeoff(self, use_ions, hyperdrive_level) -> None: ...
322
382
 
323
383
  The above creates 2 * 3 = 6 versions of ``test_takeoff``.
324
384
 
@@ -330,8 +390,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
330
390
  .. code-block:: python
331
391
 
332
392
  from itertools import product
333
- from unittest_parametrize import parametrize
334
- from unittest_parametrize import ParametrizedTestCase
393
+ from unittest_parametrize import ParametrizedTestCase, parametrize
335
394
 
336
395
 
337
396
  class RocketTests(ParametrizedTestCase):
@@ -345,8 +404,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
345
404
  )
346
405
  ),
347
406
  )
348
- def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None:
349
- ...
407
+ def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None: ...
350
408
 
351
409
  The above creates 2 * 3 * 2 = 12 versions of ``test_takeoff``.
352
410
 
@@ -370,15 +428,47 @@ To parametrize all tests within a test case, create a separate decorator and app
370
428
 
371
429
  class StatsTests(ParametrizedTestCase):
372
430
  @parametrize_race
373
- def test_strength(self, race: str) -> None:
374
- ...
431
+ def test_strength(self, race: str) -> None: ...
375
432
 
376
433
  @parametrize_race
377
- def test_dexterity(self, race: str) -> None:
378
- ...
434
+ def test_dexterity(self, race: str) -> None: ...
379
435
 
380
436
  ...
381
437
 
438
+ Pass parameters in a dataclass
439
+ ------------------------------
440
+
441
+ Thanks to `Florian Bruhin <https://bruhin.software/>`__ for this tip, from his `pytest tips and tricks presentation <https://bruhin.software/>`__.
442
+
443
+ If your test uses many parameters or cases, the parametrization may become unwieldy, as cases don’t name the arguments.
444
+ In this case, try using a `dataclass <https://docs.python.org/3/library/dataclasses.html>`__ to hold the arguments:
445
+
446
+ .. code-block:: python
447
+
448
+ from dataclasses import dataclass
449
+
450
+ from unittest_parametrize import ParametrizedTestCase, parametrize
451
+
452
+
453
+ @dataclass
454
+ class SquareParams:
455
+ x: int
456
+ expected: int
457
+
458
+
459
+ class SquareTests(ParametrizedTestCase):
460
+ @parametrize(
461
+ "sp",
462
+ [
463
+ (SquareParams(x=1, expected=1),),
464
+ (SquareParams(x=2, expected=4),),
465
+ ],
466
+ )
467
+ def test_square(self, sp: SquareParams) -> None:
468
+ self.assertEqual(sp.x**2, sp.expected)
469
+
470
+ This way, each parameter is type-checked and named, improving safety and readability.
471
+
382
472
  History
383
473
  =======
384
474
 
@@ -48,7 +48,7 @@ __ https://docs.pytest.org/en/stable/how-to/parametrize.html#parametrize-basics
48
48
  There are two steps to parametrize a test case:
49
49
 
50
50
  1. Use ``ParametrizedTestCase`` in the base classes for your test case.
51
- 2. Apply ``@parametrize`` to any tests for parametrization.
51
+ 2. Apply ``@parametrize`` to any test methods for parametrization.
52
52
  This decorator takes (at least):
53
53
 
54
54
  * the argument names to parametrize, as comma-separated string
@@ -58,8 +58,7 @@ Here’s a basic example:
58
58
 
59
59
  .. code-block:: python
60
60
 
61
- from unittest_parametrize import parametrize
62
- from unittest_parametrize import ParametrizedTestCase
61
+ from unittest_parametrize import ParametrizedTestCase, parametrize
63
62
 
64
63
 
65
64
  class SquareTests(ParametrizedTestCase):
@@ -76,6 +75,7 @@ Here’s a basic example:
76
75
  ``@parametrize`` modifies the class at definition time with Python’s |__init_subclass__ hook|__.
77
76
  It removes the original test method and creates wrapped copies with individual names.
78
77
  Thus the parametrization should work regardless of the test runner you use (be it unittest, Django’s test runner, pytest, etc.).
78
+ It supports both synchronous and asynchronous test methods.
79
79
 
80
80
  .. |__init_subclass__ hook| replace:: ``__init_subclass__`` hook
81
81
  __ https://docs.python.org/3/reference/datamodel.html#object.__init_subclass__
@@ -87,8 +87,7 @@ You can provide argument names as a sequence of strings instead:
87
87
 
88
88
  .. code-block:: python
89
89
 
90
- from unittest_parametrize import parametrize
91
- from unittest_parametrize import ParametrizedTestCase
90
+ from unittest_parametrize import ParametrizedTestCase, parametrize
92
91
 
93
92
 
94
93
  class SquareTests(ParametrizedTestCase):
@@ -151,13 +150,20 @@ You can see these names when running the tests:
151
150
 
152
151
  OK
153
152
 
154
- You can customize these names by passing ``param`` objects, which contain the arguments and an optional ID for the suffix:
153
+ You can customize these names in several ways:
154
+
155
+ 1. Using ``param`` objects with IDs.
156
+ 2. Passing a sequence of strings as the ``ids`` argument.
157
+ 3. Passing a callable as the ``ids`` argument.
158
+
159
+ Passing ``param`` objects with IDs
160
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
161
+
162
+ Pass a ``param`` object for each parameter set, setting the test ID suffix with the optional ``id`` argument:
155
163
 
156
164
  .. code-block:: python
157
165
 
158
- from unittest_parametrize import param
159
- from unittest_parametrize import parametrize
160
- from unittest_parametrize import ParametrizedTestCase
166
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
161
167
 
162
168
 
163
169
  class SquareTests(ParametrizedTestCase):
@@ -171,7 +177,7 @@ You can customize these names by passing ``param`` objects, which contain the ar
171
177
  def test_square(self, x: int, expected: int) -> None:
172
178
  self.assertEqual(x**2, expected)
173
179
 
174
- Yielding perhaps more natural names:
180
+ Yielding more natural names:
175
181
 
176
182
  .. code-block:: console
177
183
 
@@ -190,9 +196,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
190
196
 
191
197
  .. code-block:: python
192
198
 
193
- from unittest_parametrize import param
194
- from unittest_parametrize import parametrize
195
- from unittest_parametrize import ParametrizedTestCase
199
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
196
200
 
197
201
 
198
202
  class SquareTests(ParametrizedTestCase):
@@ -206,7 +210,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
206
210
  def test_square(self, x: int, expected: int) -> None:
207
211
  self.assertEqual(x**2, expected)
208
212
 
209
- ID-free ``param``\s fall back to the default index suffixes:
213
+ The ID-free ``param``\s fall back to the default index suffixes:
210
214
 
211
215
  .. code-block:: console
212
216
 
@@ -219,12 +223,14 @@ ID-free ``param``\s fall back to the default index suffixes:
219
223
 
220
224
  OK
221
225
 
222
- Alternatively, you can provide the id’s separately with the ``ids`` argument:
226
+ Passing a sequence of strings as the ``ids`` argument
227
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
228
+
229
+ Another option is to provide the IDs in the separate ``ids`` argument:
223
230
 
224
231
  .. code-block:: python
225
232
 
226
- from unittest_parametrize import parametrize
227
- from unittest_parametrize import ParametrizedTestCase
233
+ from unittest_parametrize import ParametrizedTestCase, parametrize
228
234
 
229
235
 
230
236
  class SquareTests(ParametrizedTestCase):
@@ -239,6 +245,64 @@ Alternatively, you can provide the id’s separately with the ``ids`` argument:
239
245
  def test_square(self, x: int, expected: int) -> None:
240
246
  self.assertEqual(x**2, expected)
241
247
 
248
+ This option sets the full suffixes to the provided strings:
249
+
250
+ .. code-block:: console
251
+
252
+ $ python -m unittest t.py -v
253
+ test_square_one (example.SquareTests.test_square_one) ... ok
254
+ test_square_two (example.SquareTests.test_square_two) ... ok
255
+
256
+ ----------------------------------------------------------------------
257
+ Ran 2 tests in 0.000s
258
+
259
+ OK
260
+
261
+ Passing a callable as the ``ids`` argument
262
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
263
+
264
+ The ``ids`` argument can also be a callable, which unittest-parametrize calls once per parameter value.
265
+ The callable can return a string for that value, or ``None`` to use the default index suffix.
266
+ The values are then joined with underscores to form the full suffix.
267
+
268
+ For example:
269
+
270
+ .. code-block:: python
271
+
272
+ from unittest_parametrize import ParametrizedTestCase, parametrize
273
+
274
+
275
+ def make_id(value):
276
+ if isinstance(value, int):
277
+ return f"num{value}"
278
+ return None
279
+
280
+
281
+ class SquareTests(ParametrizedTestCase):
282
+ @parametrize(
283
+ "x,expected",
284
+ [
285
+ (1, 1),
286
+ (2, 4),
287
+ ],
288
+ ids=make_id,
289
+ )
290
+ def test_square(self, x: int, expected: int) -> None:
291
+ self.assertEqual(x**2, expected)
292
+
293
+ …yields:
294
+
295
+ .. code-block:: console
296
+
297
+ $ python -m unittest t.py -v
298
+ test_square_num1_num1 (example.SquareTests.test_square_num1_num1) ... ok
299
+ test_square_num2_num4 (example.SquareTests.test_square_num2_num4) ... ok
300
+
301
+ ----------------------------------------------------------------------
302
+ Ran 2 tests in 0.000s
303
+
304
+ OK
305
+
242
306
  Use with other test decorators
243
307
  ------------------------------
244
308
 
@@ -249,8 +313,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
249
313
  .. code-block:: python
250
314
 
251
315
  from unittest import mock
252
- from unittest_parametrize import parametrize
253
- from unittest_parametrize import ParametrizedTestCase
316
+ from unittest_parametrize import ParametrizedTestCase, parametrize
254
317
 
255
318
 
256
319
  class CarpentryTests(ParametrizedTestCase):
@@ -259,8 +322,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
259
322
  [(11,), (17,)],
260
323
  )
261
324
  @mock.patch("example.hammer", autospec=True)
262
- def test_nail_a_board(self, mock_hammer, nails):
263
- ...
325
+ def test_nail_a_board(self, mock_hammer, nails): ...
264
326
 
265
327
  Also note that due to how ``mock.patch`` always adds positional arguments at the start, the parametrized arguments must come last.
266
328
  ``@parametrize`` always adds parameters as keyword arguments, so you can also use `keyword-only syntax <https://peps.python.org/pep-3102/>`__ for parametrized arguments:
@@ -268,8 +330,7 @@ Also note that due to how ``mock.patch`` always adds positional arguments at the
268
330
  .. code-block:: python
269
331
 
270
332
  # ...
271
- def test_nail_a_board(self, mock_hammer, *, nails):
272
- ...
333
+ def test_nail_a_board(self, mock_hammer, *, nails): ...
273
334
 
274
335
  Multiple ``@parametrize`` decorators
275
336
  ------------------------------------
@@ -279,8 +340,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
279
340
 
280
341
  .. code-block:: python
281
342
 
282
- from unittest_parametrize import parametrize
283
- from unittest_parametrize import ParametrizedTestCase
343
+ from unittest_parametrize import ParametrizedTestCase, parametrize
284
344
 
285
345
 
286
346
  class RocketTests(ParametrizedTestCase):
@@ -292,8 +352,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
292
352
  for hyperdrive_level in [0, 1, 2]
293
353
  ],
294
354
  )
295
- def test_takeoff(self, use_ions, hyperdrive_level) -> None:
296
- ...
355
+ def test_takeoff(self, use_ions, hyperdrive_level) -> None: ...
297
356
 
298
357
  The above creates 2 * 3 = 6 versions of ``test_takeoff``.
299
358
 
@@ -305,8 +364,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
305
364
  .. code-block:: python
306
365
 
307
366
  from itertools import product
308
- from unittest_parametrize import parametrize
309
- from unittest_parametrize import ParametrizedTestCase
367
+ from unittest_parametrize import ParametrizedTestCase, parametrize
310
368
 
311
369
 
312
370
  class RocketTests(ParametrizedTestCase):
@@ -320,8 +378,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
320
378
  )
321
379
  ),
322
380
  )
323
- def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None:
324
- ...
381
+ def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None: ...
325
382
 
326
383
  The above creates 2 * 3 * 2 = 12 versions of ``test_takeoff``.
327
384
 
@@ -345,15 +402,47 @@ To parametrize all tests within a test case, create a separate decorator and app
345
402
 
346
403
  class StatsTests(ParametrizedTestCase):
347
404
  @parametrize_race
348
- def test_strength(self, race: str) -> None:
349
- ...
405
+ def test_strength(self, race: str) -> None: ...
350
406
 
351
407
  @parametrize_race
352
- def test_dexterity(self, race: str) -> None:
353
- ...
408
+ def test_dexterity(self, race: str) -> None: ...
354
409
 
355
410
  ...
356
411
 
412
+ Pass parameters in a dataclass
413
+ ------------------------------
414
+
415
+ Thanks to `Florian Bruhin <https://bruhin.software/>`__ for this tip, from his `pytest tips and tricks presentation <https://bruhin.software/>`__.
416
+
417
+ If your test uses many parameters or cases, the parametrization may become unwieldy, as cases don’t name the arguments.
418
+ In this case, try using a `dataclass <https://docs.python.org/3/library/dataclasses.html>`__ to hold the arguments:
419
+
420
+ .. code-block:: python
421
+
422
+ from dataclasses import dataclass
423
+
424
+ from unittest_parametrize import ParametrizedTestCase, parametrize
425
+
426
+
427
+ @dataclass
428
+ class SquareParams:
429
+ x: int
430
+ expected: int
431
+
432
+
433
+ class SquareTests(ParametrizedTestCase):
434
+ @parametrize(
435
+ "sp",
436
+ [
437
+ (SquareParams(x=1, expected=1),),
438
+ (SquareParams(x=2, expected=4),),
439
+ ],
440
+ )
441
+ def test_square(self, sp: SquareParams) -> None:
442
+ self.assertEqual(sp.x**2, sp.expected)
443
+
444
+ This way, each parameter is type-checked and named, improving safety and readability.
445
+
357
446
  History
358
447
  =======
359
448
 
@@ -1,17 +1,19 @@
1
1
  [build-system]
2
2
  build-backend = "setuptools.build_meta"
3
3
  requires = [
4
- "setuptools",
4
+ "setuptools>=77",
5
5
  ]
6
6
 
7
7
  [project]
8
8
  name = "unittest-parametrize"
9
- version = "1.5.0"
9
+ version = "1.7.0"
10
10
  description = "Parametrize tests within unittest TestCases."
11
11
  readme = "README.rst"
12
12
  keywords = [
13
13
  "unittest",
14
14
  ]
15
+ license = "MIT"
16
+ license-files = [ "LICENSE" ]
15
17
  authors = [
16
18
  { name = "Adam Johnson", email = "me@adamj.eu" },
17
19
  ]
@@ -19,7 +21,6 @@ requires-python = ">=3.9"
19
21
  classifiers = [
20
22
  "Development Status :: 5 - Production/Stable",
21
23
  "Intended Audience :: Developers",
22
- "License :: OSI Approved :: MIT License",
23
24
  "Natural Language :: English",
24
25
  "Programming Language :: Python :: 3 :: Only",
25
26
  "Programming Language :: Python :: 3.9",
@@ -36,12 +37,50 @@ urls.Changelog = "https://github.com/adamchainz/unittest-parametrize/blob/main/C
36
37
  urls.Funding = "https://adamj.eu/books/"
37
38
  urls.Repository = "https://github.com/adamchainz/unittest-parametrize"
38
39
 
39
- [tool.isort]
40
- add_imports = [
41
- "from __future__ import annotations",
40
+ [dependency-groups]
41
+ test = [
42
+ "coverage[toml]",
43
+ "pytest",
44
+ "pytest-randomly",
45
+ "typing-extensions; python_version<'3.10'",
46
+ ]
47
+
48
+ [tool.ruff]
49
+ lint.select = [
50
+ # flake8-bugbear
51
+ "B",
52
+ # flake8-comprehensions
53
+ "C4",
54
+ # pycodestyle
55
+ "E",
56
+ # Pyflakes errors
57
+ "F",
58
+ # isort
59
+ "I",
60
+ # flake8-simplify
61
+ "SIM",
62
+ # flake8-tidy-imports
63
+ "TID",
64
+ # pyupgrade
65
+ "UP",
66
+ # Pyflakes warnings
67
+ "W",
68
+ ]
69
+ lint.ignore = [
70
+ # flake8-bugbear opinionated rules
71
+ "B9",
72
+ # line-too-long
73
+ "E501",
74
+ # suppressible-exception
75
+ "SIM105",
76
+ # if-else-block-instead-of-if-exp
77
+ "SIM108",
78
+ ]
79
+ lint.extend-safe-fixes = [
80
+ # non-pep585-annotation
81
+ "UP006",
42
82
  ]
43
- force_single_line = true
44
- profile = "black"
83
+ lint.isort.required-imports = [ "from __future__ import annotations" ]
45
84
 
46
85
  [tool.pyproject-fmt]
47
86
  max_supported_python = "3.13"
@@ -5,9 +5,7 @@ import sys
5
5
  from collections.abc import Sequence
6
6
  from functools import wraps
7
7
  from types import FunctionType
8
- from typing import Any
9
- from typing import Callable
10
- from typing import TypeVar
8
+ from typing import Any, Callable, TypeVar
11
9
  from unittest import TestCase
12
10
 
13
11
  if sys.version_info >= (3, 10):
@@ -40,23 +38,49 @@ class ParametrizedTestCase(TestCase):
40
38
  for param in _parametrized.params:
41
39
  params = dict(zip(_parametrized.argnames, param.args))
42
40
 
43
- @wraps(func)
44
- def test(
45
- self: TestCase,
46
- *args: Any,
47
- _func: FunctionType = func,
48
- _params: dict[str, Any] = params,
49
- **kwargs: Any,
50
- ) -> Any:
51
- try:
52
- return _func(self, *args, **_params, **kwargs)
53
- except Exception as exc:
54
- if sys.version_info >= (3, 11):
55
- exc.add_note(
56
- "Test parameters: "
57
- + ", ".join(f"{k}={v!r}" for k, v in _params.items())
58
- )
59
- raise
41
+ if inspect.iscoroutinefunction(func):
42
+
43
+ @wraps(func)
44
+ async def test(
45
+ self: TestCase,
46
+ *args: Any,
47
+ _func: FunctionType = func,
48
+ _params: dict[str, Any] = params,
49
+ **kwargs: Any,
50
+ ) -> Any:
51
+ try:
52
+ return await _func(self, *args, **_params, **kwargs)
53
+ except Exception as exc:
54
+ if sys.version_info >= (3, 11):
55
+ exc.add_note(
56
+ "Test parameters: "
57
+ + ", ".join(
58
+ f"{k}={v!r}" for k, v in _params.items()
59
+ )
60
+ )
61
+ raise
62
+
63
+ else:
64
+
65
+ @wraps(func)
66
+ def test(
67
+ self: TestCase,
68
+ *args: Any,
69
+ _func: FunctionType = func,
70
+ _params: dict[str, Any] = params,
71
+ **kwargs: Any,
72
+ ) -> Any:
73
+ try:
74
+ return _func(self, *args, **_params, **kwargs)
75
+ except Exception as exc:
76
+ if sys.version_info >= (3, 11):
77
+ exc.add_note(
78
+ "Test parameters: "
79
+ + ", ".join(
80
+ f"{k}={v!r}" for k, v in _params.items()
81
+ )
82
+ )
83
+ raise
60
84
 
61
85
  test.__name__ = f"{name}_{param.id}"
62
86
  test.__qualname__ = f"{test.__qualname__}_{param.id}"
@@ -96,8 +120,8 @@ TestFunc = Callable[P, T]
96
120
 
97
121
  def parametrize(
98
122
  argnames: str | Sequence[str],
99
- argvalues: Sequence[tuple[Any, ...]] | Sequence[param],
100
- ids: Sequence[str | None] | None = None,
123
+ argvalues: Sequence[tuple[Any, ...] | param],
124
+ ids: Sequence[str | None] | Callable[[Any], str | None] | None = None,
101
125
  ) -> Callable[[Callable[P, T]], Callable[P, T]]:
102
126
  if isinstance(argnames, str):
103
127
  argnames = [a.strip() for a in argnames.split(",")]
@@ -105,24 +129,22 @@ def parametrize(
105
129
  if len(argnames) == 0:
106
130
  raise ValueError("argnames must contain at least one element")
107
131
 
108
- if ids is not None and len(ids) != len(argvalues):
132
+ ids_callable = callable(ids)
133
+ if ids is not None and not ids_callable and len(ids) != len(argvalues): # type: ignore[arg-type]
109
134
  raise ValueError("ids must have the same length as argvalues")
110
135
 
111
136
  seen_ids = set()
112
137
  params = []
113
138
  for i, argvalue in enumerate(argvalues):
114
- if ids and ids[i]:
115
- id_ = ids[i]
116
- else:
117
- id_ = str(i)
118
-
119
139
  if isinstance(argvalue, tuple):
120
140
  if len(argvalue) != len(argnames):
121
141
  raise ValueError(
122
142
  f"tuple at index {i} has wrong number of arguments "
123
143
  + f"({len(argvalue)} != {len(argnames)})"
124
144
  )
125
- params.append(param(*argvalue, id=id_))
145
+ argvalue = param(*argvalue, id=make_id(i, argvalue, ids))
146
+ params.append(argvalue)
147
+ seen_ids.add(argvalue.id)
126
148
  elif isinstance(argvalue, param):
127
149
  if len(argvalue.args) != len(argnames):
128
150
  raise ValueError(
@@ -131,7 +153,7 @@ def parametrize(
131
153
  )
132
154
 
133
155
  if argvalue.id is None:
134
- argvalue = param(*argvalue.args, id=id_)
156
+ argvalue = param(*argvalue.args, id=make_id(i, argvalue, ids))
135
157
  if argvalue.id in seen_ids:
136
158
  raise ValueError(f"Duplicate param id {argvalue.id!r}")
137
159
  seen_ids.add(argvalue.id)
@@ -157,3 +179,34 @@ def parametrize(
157
179
  return func
158
180
 
159
181
  return wrapper
182
+
183
+
184
+ def make_id(
185
+ i: int,
186
+ argvalue: tuple[Any, ...] | param,
187
+ ids: Sequence[str | None] | Callable[[Any], str | None] | None,
188
+ ) -> str:
189
+ if callable(ids):
190
+ if isinstance(argvalue, tuple):
191
+ values = argvalue
192
+ else:
193
+ values = argvalue.args
194
+
195
+ id_parts = []
196
+ for value in values:
197
+ id_part = ids(value)
198
+ if id_part is not None:
199
+ id_parts.append(id_part)
200
+ else:
201
+ id_parts.append(str(value))
202
+ id_ = "_".join(id_parts)
203
+ # Validate the generated ID
204
+ if not f"_{id_}".isidentifier():
205
+ raise ValueError(
206
+ f"callable ids returned invalid Python identifier suffix: {id_!r}"
207
+ )
208
+ return id_
209
+ elif ids and ids[i]:
210
+ return str(ids[i])
211
+ else:
212
+ return str(i)
@@ -1,15 +1,15 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: unittest-parametrize
3
- Version: 1.5.0
3
+ Version: 1.7.0
4
4
  Summary: Parametrize tests within unittest TestCases.
5
5
  Author-email: Adam Johnson <me@adamj.eu>
6
+ License-Expression: MIT
6
7
  Project-URL: Changelog, https://github.com/adamchainz/unittest-parametrize/blob/main/CHANGELOG.rst
7
8
  Project-URL: Funding, https://adamj.eu/books/
8
9
  Project-URL: Repository, https://github.com/adamchainz/unittest-parametrize
9
10
  Keywords: unittest
10
11
  Classifier: Development Status :: 5 - Production/Stable
11
12
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Natural Language :: English
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Classifier: Programming Language :: Python :: 3.9
@@ -22,6 +22,7 @@ Requires-Python: >=3.9
22
22
  Description-Content-Type: text/x-rst
23
23
  License-File: LICENSE
24
24
  Requires-Dist: typing-extensions; python_version < "3.10"
25
+ Dynamic: license-file
25
26
 
26
27
  ====================
27
28
  unittest-parametrize
@@ -73,7 +74,7 @@ __ https://docs.pytest.org/en/stable/how-to/parametrize.html#parametrize-basics
73
74
  There are two steps to parametrize a test case:
74
75
 
75
76
  1. Use ``ParametrizedTestCase`` in the base classes for your test case.
76
- 2. Apply ``@parametrize`` to any tests for parametrization.
77
+ 2. Apply ``@parametrize`` to any test methods for parametrization.
77
78
  This decorator takes (at least):
78
79
 
79
80
  * the argument names to parametrize, as comma-separated string
@@ -83,8 +84,7 @@ Here’s a basic example:
83
84
 
84
85
  .. code-block:: python
85
86
 
86
- from unittest_parametrize import parametrize
87
- from unittest_parametrize import ParametrizedTestCase
87
+ from unittest_parametrize import ParametrizedTestCase, parametrize
88
88
 
89
89
 
90
90
  class SquareTests(ParametrizedTestCase):
@@ -101,6 +101,7 @@ Here’s a basic example:
101
101
  ``@parametrize`` modifies the class at definition time with Python’s |__init_subclass__ hook|__.
102
102
  It removes the original test method and creates wrapped copies with individual names.
103
103
  Thus the parametrization should work regardless of the test runner you use (be it unittest, Django’s test runner, pytest, etc.).
104
+ It supports both synchronous and asynchronous test methods.
104
105
 
105
106
  .. |__init_subclass__ hook| replace:: ``__init_subclass__`` hook
106
107
  __ https://docs.python.org/3/reference/datamodel.html#object.__init_subclass__
@@ -112,8 +113,7 @@ You can provide argument names as a sequence of strings instead:
112
113
 
113
114
  .. code-block:: python
114
115
 
115
- from unittest_parametrize import parametrize
116
- from unittest_parametrize import ParametrizedTestCase
116
+ from unittest_parametrize import ParametrizedTestCase, parametrize
117
117
 
118
118
 
119
119
  class SquareTests(ParametrizedTestCase):
@@ -176,13 +176,20 @@ You can see these names when running the tests:
176
176
 
177
177
  OK
178
178
 
179
- You can customize these names by passing ``param`` objects, which contain the arguments and an optional ID for the suffix:
179
+ You can customize these names in several ways:
180
+
181
+ 1. Using ``param`` objects with IDs.
182
+ 2. Passing a sequence of strings as the ``ids`` argument.
183
+ 3. Passing a callable as the ``ids`` argument.
184
+
185
+ Passing ``param`` objects with IDs
186
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
187
+
188
+ Pass a ``param`` object for each parameter set, setting the test ID suffix with the optional ``id`` argument:
180
189
 
181
190
  .. code-block:: python
182
191
 
183
- from unittest_parametrize import param
184
- from unittest_parametrize import parametrize
185
- from unittest_parametrize import ParametrizedTestCase
192
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
186
193
 
187
194
 
188
195
  class SquareTests(ParametrizedTestCase):
@@ -196,7 +203,7 @@ You can customize these names by passing ``param`` objects, which contain the ar
196
203
  def test_square(self, x: int, expected: int) -> None:
197
204
  self.assertEqual(x**2, expected)
198
205
 
199
- Yielding perhaps more natural names:
206
+ Yielding more natural names:
200
207
 
201
208
  .. code-block:: console
202
209
 
@@ -215,9 +222,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
215
222
 
216
223
  .. code-block:: python
217
224
 
218
- from unittest_parametrize import param
219
- from unittest_parametrize import parametrize
220
- from unittest_parametrize import ParametrizedTestCase
225
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
221
226
 
222
227
 
223
228
  class SquareTests(ParametrizedTestCase):
@@ -231,7 +236,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
231
236
  def test_square(self, x: int, expected: int) -> None:
232
237
  self.assertEqual(x**2, expected)
233
238
 
234
- ID-free ``param``\s fall back to the default index suffixes:
239
+ The ID-free ``param``\s fall back to the default index suffixes:
235
240
 
236
241
  .. code-block:: console
237
242
 
@@ -244,12 +249,14 @@ ID-free ``param``\s fall back to the default index suffixes:
244
249
 
245
250
  OK
246
251
 
247
- Alternatively, you can provide the id’s separately with the ``ids`` argument:
252
+ Passing a sequence of strings as the ``ids`` argument
253
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
254
+
255
+ Another option is to provide the IDs in the separate ``ids`` argument:
248
256
 
249
257
  .. code-block:: python
250
258
 
251
- from unittest_parametrize import parametrize
252
- from unittest_parametrize import ParametrizedTestCase
259
+ from unittest_parametrize import ParametrizedTestCase, parametrize
253
260
 
254
261
 
255
262
  class SquareTests(ParametrizedTestCase):
@@ -264,6 +271,64 @@ Alternatively, you can provide the id’s separately with the ``ids`` argument:
264
271
  def test_square(self, x: int, expected: int) -> None:
265
272
  self.assertEqual(x**2, expected)
266
273
 
274
+ This option sets the full suffixes to the provided strings:
275
+
276
+ .. code-block:: console
277
+
278
+ $ python -m unittest t.py -v
279
+ test_square_one (example.SquareTests.test_square_one) ... ok
280
+ test_square_two (example.SquareTests.test_square_two) ... ok
281
+
282
+ ----------------------------------------------------------------------
283
+ Ran 2 tests in 0.000s
284
+
285
+ OK
286
+
287
+ Passing a callable as the ``ids`` argument
288
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
289
+
290
+ The ``ids`` argument can also be a callable, which unittest-parametrize calls once per parameter value.
291
+ The callable can return a string for that value, or ``None`` to use the default index suffix.
292
+ The values are then joined with underscores to form the full suffix.
293
+
294
+ For example:
295
+
296
+ .. code-block:: python
297
+
298
+ from unittest_parametrize import ParametrizedTestCase, parametrize
299
+
300
+
301
+ def make_id(value):
302
+ if isinstance(value, int):
303
+ return f"num{value}"
304
+ return None
305
+
306
+
307
+ class SquareTests(ParametrizedTestCase):
308
+ @parametrize(
309
+ "x,expected",
310
+ [
311
+ (1, 1),
312
+ (2, 4),
313
+ ],
314
+ ids=make_id,
315
+ )
316
+ def test_square(self, x: int, expected: int) -> None:
317
+ self.assertEqual(x**2, expected)
318
+
319
+ …yields:
320
+
321
+ .. code-block:: console
322
+
323
+ $ python -m unittest t.py -v
324
+ test_square_num1_num1 (example.SquareTests.test_square_num1_num1) ... ok
325
+ test_square_num2_num4 (example.SquareTests.test_square_num2_num4) ... ok
326
+
327
+ ----------------------------------------------------------------------
328
+ Ran 2 tests in 0.000s
329
+
330
+ OK
331
+
267
332
  Use with other test decorators
268
333
  ------------------------------
269
334
 
@@ -274,8 +339,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
274
339
  .. code-block:: python
275
340
 
276
341
  from unittest import mock
277
- from unittest_parametrize import parametrize
278
- from unittest_parametrize import ParametrizedTestCase
342
+ from unittest_parametrize import ParametrizedTestCase, parametrize
279
343
 
280
344
 
281
345
  class CarpentryTests(ParametrizedTestCase):
@@ -284,8 +348,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
284
348
  [(11,), (17,)],
285
349
  )
286
350
  @mock.patch("example.hammer", autospec=True)
287
- def test_nail_a_board(self, mock_hammer, nails):
288
- ...
351
+ def test_nail_a_board(self, mock_hammer, nails): ...
289
352
 
290
353
  Also note that due to how ``mock.patch`` always adds positional arguments at the start, the parametrized arguments must come last.
291
354
  ``@parametrize`` always adds parameters as keyword arguments, so you can also use `keyword-only syntax <https://peps.python.org/pep-3102/>`__ for parametrized arguments:
@@ -293,8 +356,7 @@ Also note that due to how ``mock.patch`` always adds positional arguments at the
293
356
  .. code-block:: python
294
357
 
295
358
  # ...
296
- def test_nail_a_board(self, mock_hammer, *, nails):
297
- ...
359
+ def test_nail_a_board(self, mock_hammer, *, nails): ...
298
360
 
299
361
  Multiple ``@parametrize`` decorators
300
362
  ------------------------------------
@@ -304,8 +366,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
304
366
 
305
367
  .. code-block:: python
306
368
 
307
- from unittest_parametrize import parametrize
308
- from unittest_parametrize import ParametrizedTestCase
369
+ from unittest_parametrize import ParametrizedTestCase, parametrize
309
370
 
310
371
 
311
372
  class RocketTests(ParametrizedTestCase):
@@ -317,8 +378,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
317
378
  for hyperdrive_level in [0, 1, 2]
318
379
  ],
319
380
  )
320
- def test_takeoff(self, use_ions, hyperdrive_level) -> None:
321
- ...
381
+ def test_takeoff(self, use_ions, hyperdrive_level) -> None: ...
322
382
 
323
383
  The above creates 2 * 3 = 6 versions of ``test_takeoff``.
324
384
 
@@ -330,8 +390,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
330
390
  .. code-block:: python
331
391
 
332
392
  from itertools import product
333
- from unittest_parametrize import parametrize
334
- from unittest_parametrize import ParametrizedTestCase
393
+ from unittest_parametrize import ParametrizedTestCase, parametrize
335
394
 
336
395
 
337
396
  class RocketTests(ParametrizedTestCase):
@@ -345,8 +404,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
345
404
  )
346
405
  ),
347
406
  )
348
- def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None:
349
- ...
407
+ def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None: ...
350
408
 
351
409
  The above creates 2 * 3 * 2 = 12 versions of ``test_takeoff``.
352
410
 
@@ -370,15 +428,47 @@ To parametrize all tests within a test case, create a separate decorator and app
370
428
 
371
429
  class StatsTests(ParametrizedTestCase):
372
430
  @parametrize_race
373
- def test_strength(self, race: str) -> None:
374
- ...
431
+ def test_strength(self, race: str) -> None: ...
375
432
 
376
433
  @parametrize_race
377
- def test_dexterity(self, race: str) -> None:
378
- ...
434
+ def test_dexterity(self, race: str) -> None: ...
379
435
 
380
436
  ...
381
437
 
438
+ Pass parameters in a dataclass
439
+ ------------------------------
440
+
441
+ Thanks to `Florian Bruhin <https://bruhin.software/>`__ for this tip, from his `pytest tips and tricks presentation <https://bruhin.software/>`__.
442
+
443
+ If your test uses many parameters or cases, the parametrization may become unwieldy, as cases don’t name the arguments.
444
+ In this case, try using a `dataclass <https://docs.python.org/3/library/dataclasses.html>`__ to hold the arguments:
445
+
446
+ .. code-block:: python
447
+
448
+ from dataclasses import dataclass
449
+
450
+ from unittest_parametrize import ParametrizedTestCase, parametrize
451
+
452
+
453
+ @dataclass
454
+ class SquareParams:
455
+ x: int
456
+ expected: int
457
+
458
+
459
+ class SquareTests(ParametrizedTestCase):
460
+ @parametrize(
461
+ "sp",
462
+ [
463
+ (SquareParams(x=1, expected=1),),
464
+ (SquareParams(x=2, expected=4),),
465
+ ],
466
+ )
467
+ def test_square(self, sp: SquareParams) -> None:
468
+ self.assertEqual(sp.x**2, sp.expected)
469
+
470
+ This way, each parameter is type-checked and named, improving safety and readability.
471
+
382
472
  History
383
473
  =======
384
474