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