Skip to content

Commit 10fe54c

Browse files
Abseil Teamcopybara-github
authored andcommitted
Add a @parameterized.named_product which combines both named_parameters and product together.
Named Parameterized Test Cases of a Cartesian Product ====================================================== Both `named_parameters` and `product` have useful features as described above. However, when using both, it can be difficult to ensure that generated test cases retain useful names. Here, we combine both approaches to create parameterized tests with both generated permutations and human-readable names. Example: ```python @parameterized.named_product( [ dict( testcase_name='five_mod_three_is_2', num=5, modulo=3, expected=2, ), dict( testcase_name='seven_mod_four_is_3', num=7, modulo=4, expected=3, ), ], [ dict(testcase_name='int', dtype=int), dict(testcase_name='float', dtype=float), ] ) def testModuloResult(self, num, modulo, expected, dtype): self.assertEqual(expected, dtype(num) % modulo) This would generate the test cases: testModuloResult_five_mod_three_is_2_int testModuloResult_five_mod_three_is_2_float testModuloResult_seven_mod_four_is_3_int testModuloResult_seven_mod_four_is_3_float ``` PiperOrigin-RevId: 824463104
1 parent 0ae13d2 commit 10fe54c

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

absl/testing/parameterized.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,50 @@ def testModuloResult(self, num, modulo, expected, dtype):
191191
data (supplied as kwarg dicts) and for each of the two data types (supplied as
192192
a named parameter). Multiple keyword argument dicts may be supplied if required.
193193
194+
195+
Named Parameterized Test Cases of a Cartesian Product
196+
======================================================
197+
198+
Both named_parameters and product have useful features as described above.
199+
However, when using both, it can be difficult to ensure that generated test
200+
cases retain useful names.
201+
202+
Here, we combine both approaches to create parameterized tests with both
203+
generated permutations and human-readable names.
204+
205+
Example:
206+
207+
@parameterized.named_product(
208+
[
209+
dict(
210+
testcase_name='five_mod_three_is_2',
211+
num=5,
212+
modulo=3,
213+
expected=2,
214+
),
215+
dict(
216+
testcase_name='seven_mod_four_is_3',
217+
num=7,
218+
modulo=4,
219+
expected=3,
220+
),
221+
],
222+
[
223+
dict(testcase_name='int', dtype=int),
224+
dict(testcase_name='float', dtype=float),
225+
]
226+
)
227+
def testModuloResult(self, num, modulo, expected, dtype):
228+
self.assertEqual(expected, dtype(num) % modulo)
229+
230+
This would generate the test cases:
231+
232+
testModuloResult_five_mod_three_is_2_int
233+
testModuloResult_five_mod_three_is_2_float
234+
testModuloResult_seven_mod_four_is_3_int
235+
testModuloResult_seven_mod_four_is_3_float
236+
237+
194238
Async Support
195239
=============
196240
@@ -211,11 +255,13 @@ async def testSomeAsyncFunction(self, arg, expected):
211255
"""
212256

213257
from collections import abc
258+
from collections.abc import Iterator, Mapping, Sequence
214259
import functools
215260
import inspect
216261
import itertools
217262
import re
218263
import types
264+
from typing import Any
219265
import unittest
220266
import warnings
221267

@@ -486,6 +532,63 @@ def named_parameters(*testcases):
486532
return _parameter_decorator(_NAMED, testcases)
487533

488534

535+
def named_product(*kwargs_seqs):
536+
"""Decorates a test method to run it over the cartesian product of parameters.
537+
538+
See the module docstring for a usage example. The test will be run for every
539+
possible combination of the parameters.
540+
541+
For example, named_product(
542+
[
543+
dict(testcase_name="foo", x=1, y=2),
544+
dict(testcase_name="bar", x=3, y=4),
545+
],
546+
[
547+
dict(testcase_name="baz", z=5),
548+
dict(testcase_name="qux", z=6),
549+
],
550+
)
551+
is equivalent to: named_parameters(
552+
[
553+
dict(testcase_name="foo_baz", x=1, y=2, z=5),
554+
dict(testcase_name="foo_qux", x=1, y=2, z=6),
555+
dict(testcase_name="bar_baz", x=3, y=4, z=5),
556+
dict(testcase_name="bar_qux", x=3, y=4, z=6),
557+
],
558+
)
559+
560+
Args:
561+
*kwargs_seqs: Each positional parameter is a sequence of keyword arg dicts;
562+
every test case generated will include exactly one kwargs dict from each
563+
positional parameter; these will then be merged to form an overall list of
564+
arguments for the test case.
565+
566+
Returns:
567+
A test generator to be handled by TestGeneratorMetaclass.
568+
"""
569+
if len(kwargs_seqs) == 1:
570+
raise ValueError('Need at least 2 arguments for cross product.')
571+
572+
def _cross_join(seqs):
573+
for dicts in itertools.product(*seqs):
574+
testcase_names = map(lambda d: d['testcase_name'], dicts)
575+
d = dict()
576+
keys = set()
577+
for v in dicts:
578+
duplicate_keys = keys & v.keys() - {'testcase_name'}
579+
if duplicate_keys:
580+
raise ValueError(
581+
f"Duplicate keys in {v['testcase_name']}: {duplicate_keys}"
582+
)
583+
keys.update(v.keys())
584+
d.update(v)
585+
d['testcase_name'] = '_'.join(testcase_names)
586+
yield d
587+
588+
testcases = list(_cross_join(kwargs_seqs))
589+
return _parameter_decorator(_NAMED, testcases)
590+
591+
489592
def product(*kwargs_seqs, **testgrid):
490593
"""A decorator for running tests over cartesian product of parameters values.
491594

absl/testing/tests/parameterized_test.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,23 @@ def test_mixed_something(self, case):
229229
def test_without_parameters(self):
230230
pass
231231

232+
class NamedProductTests(parameterized.TestCase):
233+
234+
@parameterized.named_product(
235+
[
236+
dict(testcase_name='a_1', x=[1, 2], y=[3, 4]),
237+
dict(testcase_name='a_2', x=[5, 6], y=[7, 8]),
238+
],
239+
[
240+
dict(testcase_name='b_1', z=['foo', 'bar'], w=['baz', 'qux']),
241+
dict(
242+
testcase_name='b_2', z=['quux', 'quuz'], w=['corge', 'grault']
243+
),
244+
],
245+
)
246+
def test_named_product(self, x, y, z, w):
247+
pass
248+
232249
class ChainedTests(parameterized.TestCase):
233250

234251
@dict_decorator('cone', 'waffle')
@@ -825,6 +842,38 @@ class _(parameterized.TestCase):
825842
def test_something(self, unused_obj):
826843
pass
827844

845+
def test_named_product_duplicate_keys_fails(self):
846+
with self.assertRaises(ValueError):
847+
848+
class _(parameterized.TestCase):
849+
850+
@parameterized.named_product(
851+
[
852+
{'testcase_name': 'foo', 'x': 1, 'y': 2},
853+
{'testcase_name': 'bar', 'x': 3, 'y': 4},
854+
],
855+
[
856+
{'testcase_name': 'baz', 'x': 5, 'z': 7},
857+
{'testcase_name': 'qux', 'x': 6, 'z': 8},
858+
],
859+
)
860+
def test_something(self, x, y, z):
861+
pass
862+
863+
def test_named_product_creates_expected_tests(self):
864+
ts = unittest.defaultTestLoader.loadTestsFromTestCase(
865+
self.NamedProductTests
866+
)
867+
test = next(t for t in ts)
868+
self.assertIn('test_named_product_a_1_b_1', dir(test))
869+
self.assertIn('test_named_product_a_1_b_2', dir(test))
870+
self.assertIn('test_named_product_a_2_b_1', dir(test))
871+
self.assertIn('test_named_product_a_2_b_2', dir(test))
872+
res = unittest.TestResult()
873+
ts.run(res)
874+
self.assertEqual(4, res.testsRun)
875+
self.assertTrue(res.wasSuccessful())
876+
828877
def test_parameterized_test_iter_has_testcases_property(self):
829878
@parameterized.parameters(1, 2, 3, 4, 5, 6)
830879
def test_something(unused_self, unused_obj): # pylint: disable=invalid-name

0 commit comments

Comments
 (0)