| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | """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 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) |
| | |
| | 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 |
| |
|
| | 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): |
| | @functools.wraps(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): |
| | @functools.wraps(test_method) |
| | 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: |
| | |
| | |
| | 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) |
| | |
| | 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 |
| | |
| | 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 isinstance(testcase_params, types.GeneratorType): |
| | testcase_params = tuple(testcase_params) |
| | |
| | |
| | |
| | |
| | |
| | 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,)) |
| | |
| | |
| | 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)): |
| | |
| | |
| | |
| | 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)))) |
| |
|
| | |
| | |
| | |
| | testgrid = (tuple({k: v} for v in vs) for k, vs in testgrid.items()) |
| | testgrid = tuple(kwargs_seqs) + tuple(testgrid) |
| |
|
| | |
| | |
| | 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): |
| | |
| | |
| | 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)): |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if isinstance(obj, _ParameterizedTestIter): |
| | |
| | |
| | |
| | |
| | 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) |
| | |
| | |
| | for base in bases: |
| | |
| | |
| | |
| | |
| | |
| | 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_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.""" |
| |
|
| | |
| | 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: |
| | |
| | |
| | |
| | |
| | return '{} {}'.format(base, params_repr) |
| | else: |
| | return base |
| |
|
| |
|
| | |
| | def CoopTestCase(other_base_class): |
| | """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. |
| | """ |
| | metaclass = type( |
| | 'CoopMetaclass', |
| | (other_base_class.__metaclass__, |
| | TestGeneratorMetaclass), {}) |
| | return metaclass( |
| | 'CoopTestCase', |
| | (other_base_class, TestCase), {}) |
| |
|