diff options
author | Abseil Team <absl-team@google.com> | 2020-12-04 05:24:48 -0800 |
---|---|---|
committer | Copybara-Service <copybara-worker@google.com> | 2020-12-04 05:25:12 -0800 |
commit | de0fad7050bb51581228eeb8dd5ad4fc2fe5f88c (patch) | |
tree | a33bbaa6a5c84afba781a913b357e8839034a9f1 /absl/testing | |
parent | 74ac684cba53c7edf8440b639b0ee8ed2186d05c (diff) | |
download | absl-py-de0fad7050bb51581228eeb8dd5ad4fc2fe5f88c.tar.gz |
Adds `parameterized.product`: generates the Cartesian product of tests from multiple parameters.
This makes it easier to take a test grid of parameters and generate tests for
each combination. While the usual `parameters` function _can_ do this, it
usually can't be fit into the decorator expression, and the extra logic
to produce the product obscures the intention.
Example usage:
```
class TestModuloExample(parameterized.TestCase):
@parameterized.product(
num=[0, 10, 20],
modulo=[2, 4],
expected=[0]
)
def testModuloResult(self, num, modulo, expected):
self.assertEqual(expected, num % modulo)
```
PiperOrigin-RevId: 345657677
Change-Id: I7a3dfc868dba291a48c9d3b0e9b77921a3db494f
Diffstat (limited to 'absl/testing')
-rw-r--r-- | absl/testing/parameterized.py | 49 | ||||
-rw-r--r-- | absl/testing/tests/parameterized_test.py | 78 |
2 files changed, 127 insertions, 0 deletions
diff --git a/absl/testing/parameterized.py b/absl/testing/parameterized.py index 46b6b48..894cf6c 100644 --- a/absl/testing/parameterized.py +++ b/absl/testing/parameterized.py @@ -155,6 +155,22 @@ inside a tuple: self.assertEqual(0, sum(arg)) +Cartesian product of Parameter Values as Parametrized Test Cases +====================================================== +If required to test method over a cartesian product of parameters, +`parameterized.product` may be used to facilitate generation of parameters +test combinations: + + class TestModuloExample(parameterized.TestCase): + @parameterized.product( + num=[0, 20, 80], + modulo=[2, 4], + expected=[0] + ) + def testModuloResult(self, num, modulo, expected): + self.assertEqual(expected, num % modulo) + + Async Support =============================== If a test needs to call async functions, it can inherit from both @@ -178,6 +194,7 @@ from __future__ import division from __future__ import print_function import functools +import itertools import re import types import unittest @@ -447,6 +464,38 @@ def named_parameters(*testcases): return _parameter_decorator(_NAMED, testcases) +def product(**testgrid): + """A decorator for running tests over cartesian product of parameters values. + + See the module docstring for a usage example. The test will be run for every + possible combination of the parameters. + + Args: + **testgrid: A mapping of parameter names and their possible values. + Possible values should given as either a list or a tuple. + + Raises: + NoTestsError: Raised when the decorator generates no tests. + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + + for name, values in testgrid.items(): + assert isinstance(values, list) or isinstance(values, tuple), ( + 'Values of {} must be given as list or tuple, found {}'.format( + name, type(values))) + + # Create all possible combinations of parameters as a cartesian product + # of parameter values. + testcases = [ + dict(zip(testgrid.keys(), product)) + for product in itertools.product(*testgrid.values()) + ] + + return _parameter_decorator(_ARGUMENT_REPR, testcases) + + class TestGeneratorMetaclass(type): """Metaclass for adding tests generated by parameterized decorators.""" diff --git a/absl/testing/tests/parameterized_test.py b/absl/testing/tests/parameterized_test.py index b75068a..b099cd3 100644 --- a/absl/testing/tests/parameterized_test.py +++ b/absl/testing/tests/parameterized_test.py @@ -494,6 +494,84 @@ class ParameterizedTestsTest(absltest.TestCase): ], short_descs) + def test_successful_product_test(self): + + class GoodProductTestCase(parameterized.TestCase): + + @parameterized.product( + num=(0, 20, 80), + modulo=(2, 4), + expected=(0,) + ) + def testModuloResult(self, num, modulo, expected): + self.assertEqual(expected, num % modulo) + + ts = unittest.makeSuite(GoodProductTestCase) + res = unittest.TestResult() + ts.run(res) + self.assertEqual(ts.countTestCases(), 6) + self.assertEqual(res.testsRun, 6) + self.assertTrue(res.wasSuccessful()) + + def test_product_recorded_failures(self): + + class MixedProductTestCase(parameterized.TestCase): + + @parameterized.product( + num=(0, 10, 20), + modulo=(2, 4), + expected=(0,) + ) + def testModuloResult(self, num, modulo, expected): + self.assertEqual(expected, num % modulo) + + ts = unittest.makeSuite(MixedProductTestCase) + self.assertEqual(6, ts.countTestCases()) + + res = unittest.TestResult() + ts.run(res) + self.assertEqual(res.testsRun, 6) + self.assertFalse(res.wasSuccessful()) + self.assertLen(res.failures, 1) + self.assertEmpty(res.errors) + + def test_mismatched_product_parameter(self): + + class MismatchedProductParam(parameterized.TestCase): + + @parameterized.product( + a=(1, 2), + mismatch=(1, 2) + ) + # will fail because of mismatch in parameter names. + def test_something(self, a, b): + pass + + ts = unittest.makeSuite(MismatchedProductParam) + res = unittest.TestResult() + ts.run(res) + self.assertEqual(res.testsRun, 4) + self.assertFalse(res.wasSuccessful()) + self.assertLen(res.errors, 4) + + def test_no_test_error_empty_product_parameter(self): + with self.assertRaises(parameterized.NoTestsError): + + class EmptyProductParam(parameterized.TestCase): # pylint: disable=unused-variable + + @parameterized.product(arg1=[1, 2], arg2=[]) + def test_something(self, arg1, arg2): + pass # not called because arg2 has empty list of values. + + def test_bad_product_parameters(self): + with self.assertRaisesRegex(AssertionError, 'must be given as list or'): + + class BadProductParams(parameterized.TestCase): # pylint: disable=unused-variable + + @parameterized.product(arg1=[1, 2], arg2='abcd') + def test_something(self, arg1, arg2): + pass # not called because arg2 is not list or tuple. + def test_generator_tests_disallowed(self): with self.assertRaisesRegex(RuntimeError, 'generated.*without'): class GeneratorTests(parameterized.TestCase): # pylint: disable=unused-variable |