Spaces:
Paused
Paused
| # Copyright 2017 The Abseil Authors. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """Adds support for parameterized tests to Python's unittest TestCase class. | |
| A parameterized test is a method in a test case that is invoked with different | |
| argument tuples. | |
| A simple example:: | |
| class AdditionExample(parameterized.TestCase): | |
| @parameterized.parameters( | |
| (1, 2, 3), | |
| (4, 5, 9), | |
| (1, 1, 3)) | |
| def testAddition(self, op1, op2, result): | |
| self.assertEqual(result, op1 + op2) | |
| Each invocation is a separate test case and properly isolated just | |
| like a normal test method, with its own setUp/tearDown cycle. In the | |
| example above, there are three separate testcases, one of which will | |
| fail due to an assertion error (1 + 1 != 3). | |
| Parameters for individual test cases can be tuples (with positional parameters) | |
| or dictionaries (with named parameters):: | |
| class AdditionExample(parameterized.TestCase): | |
| @parameterized.parameters( | |
| {'op1': 1, 'op2': 2, 'result': 3}, | |
| {'op1': 4, 'op2': 5, 'result': 9}, | |
| ) | |
| def testAddition(self, op1, op2, result): | |
| self.assertEqual(result, op1 + op2) | |
| If a parameterized test fails, the error message will show the | |
| original test name and the parameters for that test. | |
| The id method of the test, used internally by the unittest framework, is also | |
| modified to show the arguments (but note that the name reported by `id()` | |
| doesn't match the actual test name, see below). To make sure that test names | |
| stay the same across several invocations, object representations like:: | |
| >>> class Foo(object): | |
| ... pass | |
| >>> repr(Foo()) | |
| '<__main__.Foo object at 0x23d8610>' | |
| are turned into ``__main__.Foo``. When selecting a subset of test cases to run | |
| on the command-line, the test cases contain an index suffix for each argument | |
| in the order they were passed to :func:`parameters` (eg. testAddition0, | |
| testAddition1, etc.) This naming scheme is subject to change; for more reliable | |
| and stable names, especially in test logs, use :func:`named_parameters` instead. | |
| Tests using :func:`named_parameters` are similar to :func:`parameters`, except | |
| only tuples or dicts of args are supported. For tuples, the first parameter arg | |
| has to be a string (or an object that returns an apt name when converted via | |
| ``str()``). For dicts, a value for the key ``testcase_name`` must be present and | |
| must be a string (or an object that returns an apt name when converted via | |
| ``str()``):: | |
| class NamedExample(parameterized.TestCase): | |
| @parameterized.named_parameters( | |
| ('Normal', 'aa', 'aaa', True), | |
| ('EmptyPrefix', '', 'abc', True), | |
| ('BothEmpty', '', '', True)) | |
| def testStartsWith(self, prefix, string, result): | |
| self.assertEqual(result, string.startswith(prefix)) | |
| class NamedExample(parameterized.TestCase): | |
| @parameterized.named_parameters( | |
| {'testcase_name': 'Normal', | |
| 'result': True, 'string': 'aaa', 'prefix': 'aa'}, | |
| {'testcase_name': 'EmptyPrefix', | |
| 'result': True, 'string': 'abc', 'prefix': ''}, | |
| {'testcase_name': 'BothEmpty', | |
| 'result': True, 'string': '', 'prefix': ''}) | |
| def testStartsWith(self, prefix, string, result): | |
| self.assertEqual(result, string.startswith(prefix)) | |
| Named tests also have the benefit that they can be run individually | |
| from the command line:: | |
| $ testmodule.py NamedExample.testStartsWithNormal | |
| . | |
| -------------------------------------------------------------------- | |
| Ran 1 test in 0.000s | |
| OK | |
| Parameterized Classes | |
| ===================== | |
| If invocation arguments are shared across test methods in a single | |
| TestCase class, instead of decorating all test methods | |
| individually, the class itself can be decorated:: | |
| @parameterized.parameters( | |
| (1, 2, 3), | |
| (4, 5, 9)) | |
| class ArithmeticTest(parameterized.TestCase): | |
| def testAdd(self, arg1, arg2, result): | |
| self.assertEqual(arg1 + arg2, result) | |
| def testSubtract(self, arg1, arg2, result): | |
| self.assertEqual(result - arg1, arg2) | |
| Inputs from Iterables | |
| ===================== | |
| If parameters should be shared across several test cases, or are dynamically | |
| created from other sources, a single non-tuple iterable can be passed into | |
| the decorator. This iterable will be used to obtain the test cases:: | |
| class AdditionExample(parameterized.TestCase): | |
| @parameterized.parameters( | |
| c.op1, c.op2, c.result for c in testcases | |
| ) | |
| def testAddition(self, op1, op2, result): | |
| self.assertEqual(result, op1 + op2) | |
| Single-Argument Test Methods | |
| ============================ | |
| If a test method takes only one argument, the single arguments must not be | |
| wrapped into a tuple:: | |
| class NegativeNumberExample(parameterized.TestCase): | |
| @parameterized.parameters( | |
| -1, -3, -4, -5 | |
| ) | |
| def testIsNegative(self, arg): | |
| self.assertTrue(IsNegative(arg)) | |
| List/tuple as a Single Argument | |
| =============================== | |
| If a test method takes a single argument of a list/tuple, it must be wrapped | |
| inside a tuple:: | |
| class ZeroSumExample(parameterized.TestCase): | |
| @parameterized.parameters( | |
| ([-1, 0, 1], ), | |
| ([-2, 0, 2], ), | |
| ) | |
| def testSumIsZero(self, arg): | |
| self.assertEqual(0, sum(arg)) | |
| Cartesian product of Parameter Values as Parameterized 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) | |
| This results in 6 test cases being created - one for each combination of the | |
| parameters. It is also possible to supply sequences of keyword argument dicts | |
| as elements of the cartesian product:: | |
| @parameterized.product( | |
| (dict(num=5, modulo=3, expected=2), | |
| dict(num=7, modulo=4, expected=3)), | |
| dtype=(int, float) | |
| ) | |
| def testModuloResult(self, num, modulo, expected, dtype): | |
| self.assertEqual(expected, dtype(num) % modulo) | |
| This results in 4 test cases being created - for each of the two sets of test | |
| data (supplied as kwarg dicts) and for each of the two data types (supplied as | |
| a named parameter). Multiple keyword argument dicts may be supplied if required. | |
| Async Support | |
| ============= | |
| If a test needs to call async functions, it can inherit from both | |
| parameterized.TestCase and another TestCase that supports async calls, such | |
| as [asynctest](https://github.com/Martiusweb/asynctest):: | |
| import asynctest | |
| class AsyncExample(parameterized.TestCase, asynctest.TestCase): | |
| @parameterized.parameters( | |
| ('a', 1), | |
| ('b', 2), | |
| ) | |
| async def testSomeAsyncFunction(self, arg, expected): | |
| actual = await someAsyncFunction(arg) | |
| self.assertEqual(actual, expected) | |
| """ | |
| from collections import abc | |
| import functools | |
| import inspect | |
| import itertools | |
| import re | |
| import types | |
| import unittest | |
| import warnings | |
| from absl.testing import absltest | |
| _ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') | |
| _NAMED = object() | |
| _ARGUMENT_REPR = object() | |
| _NAMED_DICT_KEY = 'testcase_name' | |
| class NoTestsError(Exception): | |
| """Raised when parameterized decorators do not generate any tests.""" | |
| class DuplicateTestNameError(Exception): | |
| """Raised when a parameterized test has the same test name multiple times.""" | |
| def __init__(self, test_class_name, new_test_name, original_test_name): | |
| super(DuplicateTestNameError, self).__init__( | |
| 'Duplicate parameterized test name in {}: generated test name {!r} ' | |
| '(generated from {!r}) already exists. Consider using ' | |
| 'named_parameters() to give your tests unique names and/or renaming ' | |
| 'the conflicting test method.'.format( | |
| test_class_name, new_test_name, original_test_name)) | |
| def _clean_repr(obj): | |
| return _ADDR_RE.sub(r'<\1>', repr(obj)) | |
| def _non_string_or_bytes_iterable(obj): | |
| return (isinstance(obj, abc.Iterable) and not isinstance(obj, str) and | |
| not isinstance(obj, bytes)) | |
| def _format_parameter_list(testcase_params): | |
| if isinstance(testcase_params, abc.Mapping): | |
| return ', '.join('%s=%s' % (argname, _clean_repr(value)) | |
| for argname, value in testcase_params.items()) | |
| elif _non_string_or_bytes_iterable(testcase_params): | |
| return ', '.join(map(_clean_repr, testcase_params)) | |
| else: | |
| return _format_parameter_list((testcase_params,)) | |
| def _async_wrapped(func): | |
| async def wrapper(*args, **kwargs): | |
| return await func(*args, **kwargs) | |
| return wrapper | |
| class _ParameterizedTestIter(object): | |
| """Callable and iterable class for producing new test cases.""" | |
| def __init__(self, test_method, testcases, naming_type, original_name=None): | |
| """Returns concrete test functions for a test and a list of parameters. | |
| The naming_type is used to determine the name of the concrete | |
| functions as reported by the unittest framework. If naming_type is | |
| _FIRST_ARG, the testcases must be tuples, and the first element must | |
| have a string representation that is a valid Python identifier. | |
| Args: | |
| test_method: The decorated test method. | |
| testcases: (list of tuple/dict) A list of parameter tuples/dicts for | |
| individual test invocations. | |
| naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. | |
| original_name: The original test method name. When decorated on a test | |
| method, None is passed to __init__ and test_method.__name__ is used. | |
| Note test_method.__name__ might be different than the original defined | |
| test method because of the use of other decorators. A more accurate | |
| value is set by TestGeneratorMetaclass.__new__ later. | |
| """ | |
| self._test_method = test_method | |
| self.testcases = testcases | |
| self._naming_type = naming_type | |
| if original_name is None: | |
| original_name = test_method.__name__ | |
| self._original_name = original_name | |
| self.__name__ = _ParameterizedTestIter.__name__ | |
| def __call__(self, *args, **kwargs): | |
| raise RuntimeError('You appear to be running a parameterized test case ' | |
| 'without having inherited from parameterized.' | |
| 'TestCase. This is bad because none of ' | |
| 'your test cases are actually being run. You may also ' | |
| 'be using another decorator before the parameterized ' | |
| 'one, in which case you should reverse the order.') | |
| def __iter__(self): | |
| test_method = self._test_method | |
| naming_type = self._naming_type | |
| def make_bound_param_test(testcase_params): | |
| def bound_param_test(self): | |
| if isinstance(testcase_params, abc.Mapping): | |
| return test_method(self, **testcase_params) | |
| elif _non_string_or_bytes_iterable(testcase_params): | |
| return test_method(self, *testcase_params) | |
| else: | |
| return test_method(self, testcase_params) | |
| if naming_type is _NAMED: | |
| # Signal the metaclass that the name of the test function is unique | |
| # and descriptive. | |
| bound_param_test.__x_use_name__ = True | |
| testcase_name = None | |
| if isinstance(testcase_params, abc.Mapping): | |
| if _NAMED_DICT_KEY not in testcase_params: | |
| raise RuntimeError( | |
| 'Dict for named tests must contain key "%s"' % _NAMED_DICT_KEY) | |
| # Create a new dict to avoid modifying the supplied testcase_params. | |
| testcase_name = testcase_params[_NAMED_DICT_KEY] | |
| testcase_params = { | |
| k: v for k, v in testcase_params.items() if k != _NAMED_DICT_KEY | |
| } | |
| elif _non_string_or_bytes_iterable(testcase_params): | |
| if not isinstance(testcase_params[0], str): | |
| raise RuntimeError( | |
| 'The first element of named test parameters is the test name ' | |
| 'suffix and must be a string') | |
| testcase_name = testcase_params[0] | |
| testcase_params = testcase_params[1:] | |
| else: | |
| raise RuntimeError( | |
| 'Named tests must be passed a dict or non-string iterable.') | |
| test_method_name = self._original_name | |
| # Support PEP-8 underscore style for test naming if used. | |
| if (test_method_name.startswith('test_') | |
| and testcase_name | |
| and not testcase_name.startswith('_')): | |
| test_method_name += '_' | |
| bound_param_test.__name__ = test_method_name + str(testcase_name) | |
| elif naming_type is _ARGUMENT_REPR: | |
| # If it's a generator, convert it to a tuple and treat them as | |
| # parameters. | |
| if isinstance(testcase_params, types.GeneratorType): | |
| testcase_params = tuple(testcase_params) | |
| # The metaclass creates a unique, but non-descriptive method name for | |
| # _ARGUMENT_REPR tests using an indexed suffix. | |
| # To keep test names descriptive, only the original method name is used. | |
| # To make sure test names are unique, we add a unique descriptive suffix | |
| # __x_params_repr__ for every test. | |
| params_repr = '(%s)' % (_format_parameter_list(testcase_params),) | |
| bound_param_test.__x_params_repr__ = params_repr | |
| else: | |
| raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) | |
| bound_param_test.__doc__ = '%s(%s)' % ( | |
| bound_param_test.__name__, _format_parameter_list(testcase_params)) | |
| if test_method.__doc__: | |
| bound_param_test.__doc__ += '\n%s' % (test_method.__doc__,) | |
| if inspect.iscoroutinefunction(test_method): | |
| return _async_wrapped(bound_param_test) | |
| return bound_param_test | |
| return (make_bound_param_test(c) for c in self.testcases) | |
| def _modify_class(class_object, testcases, naming_type): | |
| assert not getattr(class_object, '_test_params_reprs', None), ( | |
| 'Cannot add parameters to %s. Either it already has parameterized ' | |
| 'methods, or its super class is also a parameterized class.' % ( | |
| class_object,)) | |
| # NOTE: _test_params_repr is private to parameterized.TestCase and it's | |
| # metaclass; do not use it outside of those classes. | |
| class_object._test_params_reprs = test_params_reprs = {} | |
| for name, obj in class_object.__dict__.copy().items(): | |
| if (name.startswith(unittest.TestLoader.testMethodPrefix) | |
| and isinstance(obj, types.FunctionType)): | |
| delattr(class_object, name) | |
| methods = {} | |
| _update_class_dict_for_param_test_case( | |
| class_object.__name__, methods, test_params_reprs, name, | |
| _ParameterizedTestIter(obj, testcases, naming_type, name)) | |
| for meth_name, meth in methods.items(): | |
| setattr(class_object, meth_name, meth) | |
| def _parameter_decorator(naming_type, testcases): | |
| """Implementation of the parameterization decorators. | |
| Args: | |
| naming_type: The naming type. | |
| testcases: Testcase parameters. | |
| Raises: | |
| NoTestsError: Raised when the decorator generates no tests. | |
| Returns: | |
| A function for modifying the decorated object. | |
| """ | |
| def _apply(obj): | |
| if isinstance(obj, type): | |
| _modify_class(obj, testcases, naming_type) | |
| return obj | |
| else: | |
| return _ParameterizedTestIter(obj, testcases, naming_type) | |
| if (len(testcases) == 1 and | |
| not isinstance(testcases[0], tuple) and | |
| not isinstance(testcases[0], abc.Mapping)): | |
| # Support using a single non-tuple parameter as a list of test cases. | |
| # Note that the single non-tuple parameter can't be Mapping either, which | |
| # means a single dict parameter case. | |
| assert _non_string_or_bytes_iterable(testcases[0]), ( | |
| 'Single parameter argument must be a non-string non-Mapping iterable') | |
| testcases = testcases[0] | |
| if not isinstance(testcases, abc.Sequence): | |
| testcases = list(testcases) | |
| if not testcases: | |
| raise NoTestsError( | |
| 'parameterized test decorators did not generate any tests. ' | |
| 'Make sure you specify non-empty parameters, ' | |
| 'and do not reuse generators more than once.') | |
| return _apply | |
| def parameters(*testcases): | |
| """A decorator for creating parameterized tests. | |
| See the module docstring for a usage example. | |
| Args: | |
| *testcases: Parameters for the decorated method, either a single | |
| iterable, or a list of tuples/dicts/objects (for tests with only one | |
| argument). | |
| Raises: | |
| NoTestsError: Raised when the decorator generates no tests. | |
| Returns: | |
| A test generator to be handled by TestGeneratorMetaclass. | |
| """ | |
| return _parameter_decorator(_ARGUMENT_REPR, testcases) | |
| def named_parameters(*testcases): | |
| """A decorator for creating parameterized tests. | |
| See the module docstring for a usage example. For every parameter tuple | |
| passed, the first element of the tuple should be a string and will be appended | |
| to the name of the test method. Each parameter dict passed must have a value | |
| for the key "testcase_name", the string representation of that value will be | |
| appended to the name of the test method. | |
| Args: | |
| *testcases: Parameters for the decorated method, either a single iterable, | |
| or a list of tuples or dicts. | |
| Raises: | |
| NoTestsError: Raised when the decorator generates no tests. | |
| Returns: | |
| A test generator to be handled by TestGeneratorMetaclass. | |
| """ | |
| return _parameter_decorator(_NAMED, testcases) | |
| def product(*kwargs_seqs, **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: | |
| *kwargs_seqs: Each positional parameter is a sequence of keyword arg dicts; | |
| every test case generated will include exactly one kwargs dict from each | |
| positional parameter; these will then be merged to form an overall list | |
| of arguments for the test case. | |
| **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, tuple)), ( | |
| 'Values of {} must be given as list or tuple, found {}'.format( | |
| name, type(values))) | |
| prior_arg_names = set() | |
| for kwargs_seq in kwargs_seqs: | |
| assert ((isinstance(kwargs_seq, (list, tuple))) and | |
| all(isinstance(kwargs, dict) for kwargs in kwargs_seq)), ( | |
| 'Positional parameters must be a sequence of keyword arg' | |
| 'dicts, found {}' | |
| .format(kwargs_seq)) | |
| if kwargs_seq: | |
| arg_names = set(kwargs_seq[0]) | |
| assert all(set(kwargs) == arg_names for kwargs in kwargs_seq), ( | |
| 'Keyword argument dicts within a single parameter must all have the ' | |
| 'same keys, found {}'.format(kwargs_seq)) | |
| assert not (arg_names & prior_arg_names), ( | |
| 'Keyword argument dict sequences must all have distinct argument ' | |
| 'names, found duplicate(s) {}' | |
| .format(sorted(arg_names & prior_arg_names))) | |
| prior_arg_names |= arg_names | |
| assert not (prior_arg_names & set(testgrid)), ( | |
| 'Arguments supplied in kwargs dicts in positional parameters must not ' | |
| 'overlap with arguments supplied as named parameters; found duplicate ' | |
| 'argument(s) {}'.format(sorted(prior_arg_names & set(testgrid)))) | |
| # Convert testgrid into a sequence of sequences of kwargs dicts and combine | |
| # with the positional parameters. | |
| # So foo=[1,2], bar=[3,4] --> [[{foo: 1}, {foo: 2}], [{bar: 3, bar: 4}]] | |
| testgrid = (tuple({k: v} for v in vs) for k, vs in testgrid.items()) | |
| testgrid = tuple(kwargs_seqs) + tuple(testgrid) | |
| # Create all possible combinations of parameters as a cartesian product | |
| # of parameter values. | |
| testcases = [ | |
| dict(itertools.chain.from_iterable(case.items() | |
| for case in cases)) | |
| for cases in itertools.product(*testgrid) | |
| ] | |
| return _parameter_decorator(_ARGUMENT_REPR, testcases) | |
| class TestGeneratorMetaclass(type): | |
| """Metaclass for adding tests generated by parameterized decorators.""" | |
| def __new__(cls, class_name, bases, dct): | |
| # NOTE: _test_params_repr is private to parameterized.TestCase and it's | |
| # metaclass; do not use it outside of those classes. | |
| test_params_reprs = dct.setdefault('_test_params_reprs', {}) | |
| for name, obj in dct.copy().items(): | |
| if (name.startswith(unittest.TestLoader.testMethodPrefix) and | |
| _non_string_or_bytes_iterable(obj)): | |
| # NOTE: `obj` might not be a _ParameterizedTestIter in two cases: | |
| # 1. a class-level iterable named test* that isn't a test, such as | |
| # a list of something. Such attributes get deleted from the class. | |
| # | |
| # 2. If a decorator is applied to the parameterized test, e.g. | |
| # @morestuff | |
| # @parameterized.parameters(...) | |
| # def test_foo(...): ... | |
| # | |
| # This is OK so long as the underlying parameterized function state | |
| # is forwarded (e.g. using functool.wraps() and **without** | |
| # accessing explicitly accessing the internal attributes. | |
| if isinstance(obj, _ParameterizedTestIter): | |
| # Update the original test method name so it's more accurate. | |
| # The mismatch might happen when another decorator is used inside | |
| # the parameterized decrators, and the inner decorator doesn't | |
| # preserve its __name__. | |
| obj._original_name = name | |
| iterator = iter(obj) | |
| dct.pop(name) | |
| _update_class_dict_for_param_test_case( | |
| class_name, dct, test_params_reprs, name, iterator) | |
| # If the base class is a subclass of parameterized.TestCase, inherit its | |
| # _test_params_reprs too. | |
| for base in bases: | |
| # Check if the base has _test_params_reprs first, then check if it's a | |
| # subclass of parameterized.TestCase. Otherwise when this is called for | |
| # the parameterized.TestCase definition itself, this raises because | |
| # itself is not defined yet. This works as long as absltest.TestCase does | |
| # not define _test_params_reprs. | |
| base_test_params_reprs = getattr(base, '_test_params_reprs', None) | |
| if base_test_params_reprs and issubclass(base, TestCase): | |
| for test_method, test_method_id in base_test_params_reprs.items(): | |
| # test_method may both exists in base and this class. | |
| # This class's method overrides base class's. | |
| # That's why it should only inherit it if it does not exist. | |
| test_params_reprs.setdefault(test_method, test_method_id) | |
| return type.__new__(cls, class_name, bases, dct) | |
| def _update_class_dict_for_param_test_case( | |
| test_class_name, dct, test_params_reprs, name, iterator): | |
| """Adds individual test cases to a dictionary. | |
| Args: | |
| test_class_name: The name of the class tests are added to. | |
| dct: The target dictionary. | |
| test_params_reprs: The dictionary for mapping names to test IDs. | |
| name: The original name of the test case. | |
| iterator: The iterator generating the individual test cases. | |
| Raises: | |
| DuplicateTestNameError: Raised when a test name occurs multiple times. | |
| RuntimeError: If non-parameterized functions are generated. | |
| """ | |
| for idx, func in enumerate(iterator): | |
| assert callable(func), 'Test generators must yield callables, got %r' % ( | |
| func,) | |
| if not (getattr(func, '__x_use_name__', None) or | |
| getattr(func, '__x_params_repr__', None)): | |
| raise RuntimeError( | |
| '{}.{} generated a test function without using the parameterized ' | |
| 'decorators. Only tests generated using the decorators are ' | |
| 'supported.'.format(test_class_name, name)) | |
| if getattr(func, '__x_use_name__', False): | |
| original_name = func.__name__ | |
| new_name = original_name | |
| else: | |
| original_name = name | |
| new_name = '%s%d' % (original_name, idx) | |
| if new_name in dct: | |
| raise DuplicateTestNameError(test_class_name, new_name, original_name) | |
| dct[new_name] = func | |
| test_params_reprs[new_name] = getattr(func, '__x_params_repr__', '') | |
| class TestCase(absltest.TestCase, metaclass=TestGeneratorMetaclass): | |
| """Base class for test cases using the parameters decorator.""" | |
| # visibility: private; do not call outside this class. | |
| def _get_params_repr(self): | |
| return self._test_params_reprs.get(self._testMethodName, '') | |
| def __str__(self): | |
| params_repr = self._get_params_repr() | |
| if params_repr: | |
| params_repr = ' ' + params_repr | |
| return '{}{} ({})'.format( | |
| self._testMethodName, params_repr, | |
| unittest.util.strclass(self.__class__)) | |
| def id(self): | |
| """Returns the descriptive ID of the test. | |
| This is used internally by the unittesting framework to get a name | |
| for the test to be used in reports. | |
| Returns: | |
| The test id. | |
| """ | |
| base = super(TestCase, self).id() | |
| params_repr = self._get_params_repr() | |
| if params_repr: | |
| # We include the params in the id so that, when reported in the | |
| # test.xml file, the value is more informative than just "test_foo0". | |
| # Use a space to separate them so that it's copy/paste friendly and | |
| # easy to identify the actual test id. | |
| return '{} {}'.format(base, params_repr) | |
| else: | |
| return base | |
| # This function is kept CamelCase because it's used as a class's base class. | |
| def CoopTestCase(other_base_class): # pylint: disable=invalid-name | |
| """Returns a new base class with a cooperative metaclass base. | |
| This enables the TestCase to be used in combination | |
| with other base classes that have custom metaclasses, such as | |
| ``mox.MoxTestBase``. | |
| Only works with metaclasses that do not override ``type.__new__``. | |
| Example:: | |
| from absl.testing import parameterized | |
| class ExampleTest(parameterized.CoopTestCase(OtherTestCase)): | |
| ... | |
| Args: | |
| other_base_class: (class) A test case base class. | |
| Returns: | |
| A new class object. | |
| """ | |
| # If the other base class has a metaclass of 'type' then trying to combine | |
| # the metaclasses will result in an MRO error. So simply combine them and | |
| # return. | |
| if type(other_base_class) == type: # pylint: disable=unidiomatic-typecheck | |
| warnings.warn( | |
| 'CoopTestCase is only necessary when combining with a class that uses' | |
| ' a metaclass. Use multiple inheritance like this instead: class' | |
| f' ExampleTest(paramaterized.TestCase, {other_base_class.__name__}):', | |
| stacklevel=2, | |
| ) | |
| class CoopTestCaseBase(other_base_class, TestCase): | |
| pass | |
| return CoopTestCaseBase | |
| else: | |
| class CoopMetaclass(type(other_base_class), TestGeneratorMetaclass): # pylint: disable=unused-variable | |
| pass | |
| class CoopTestCaseBase(other_base_class, TestCase, metaclass=CoopMetaclass): | |
| pass | |
| return CoopTestCaseBase | |