aboutsummaryrefslogtreecommitdiff
path: root/absl/testing
diff options
context:
space:
mode:
authorAbseil Team <absl-team@google.com>2020-12-04 05:24:48 -0800
committerCopybara-Service <copybara-worker@google.com>2020-12-04 05:25:12 -0800
commitde0fad7050bb51581228eeb8dd5ad4fc2fe5f88c (patch)
treea33bbaa6a5c84afba781a913b357e8839034a9f1 /absl/testing
parent74ac684cba53c7edf8440b639b0ee8ed2186d05c (diff)
downloadabsl-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.py49
-rw-r--r--absl/testing/tests/parameterized_test.py78
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