• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2017 The Abseil Authors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Adds support for parameterized tests to Python's unittest TestCase class.
16
17A parameterized test is a method in a test case that is invoked with different
18argument tuples.
19
20A simple example::
21
22    class AdditionExample(parameterized.TestCase):
23      @parameterized.parameters(
24        (1, 2, 3),
25        (4, 5, 9),
26        (1, 1, 3))
27      def testAddition(self, op1, op2, result):
28        self.assertEqual(result, op1 + op2)
29
30Each invocation is a separate test case and properly isolated just
31like a normal test method, with its own setUp/tearDown cycle. In the
32example above, there are three separate testcases, one of which will
33fail due to an assertion error (1 + 1 != 3).
34
35Parameters for individual test cases can be tuples (with positional parameters)
36or dictionaries (with named parameters)::
37
38    class AdditionExample(parameterized.TestCase):
39      @parameterized.parameters(
40        {'op1': 1, 'op2': 2, 'result': 3},
41        {'op1': 4, 'op2': 5, 'result': 9},
42      )
43      def testAddition(self, op1, op2, result):
44        self.assertEqual(result, op1 + op2)
45
46If a parameterized test fails, the error message will show the
47original test name and the parameters for that test.
48
49The id method of the test, used internally by the unittest framework, is also
50modified to show the arguments (but note that the name reported by `id()`
51doesn't match the actual test name, see below). To make sure that test names
52stay the same across several invocations, object representations like::
53
54    >>> class Foo(object):
55    ...  pass
56    >>> repr(Foo())
57    '<__main__.Foo object at 0x23d8610>'
58
59are turned into ``__main__.Foo``. When selecting a subset of test cases to run
60on the command-line, the test cases contain an index suffix for each argument
61in the order they were passed to :func:`parameters` (eg. testAddition0,
62testAddition1, etc.) This naming scheme is subject to change; for more reliable
63and stable names, especially in test logs, use :func:`named_parameters` instead.
64
65Tests using :func:`named_parameters` are similar to :func:`parameters`, except
66only tuples or dicts of args are supported. For tuples, the first parameter arg
67has to be a string (or an object that returns an apt name when converted via
68``str()``). For dicts, a value for the key ``testcase_name`` must be present and
69must be a string (or an object that returns an apt name when converted via
70``str()``)::
71
72    class NamedExample(parameterized.TestCase):
73      @parameterized.named_parameters(
74        ('Normal', 'aa', 'aaa', True),
75        ('EmptyPrefix', '', 'abc', True),
76        ('BothEmpty', '', '', True))
77      def testStartsWith(self, prefix, string, result):
78        self.assertEqual(result, string.startswith(prefix))
79
80    class NamedExample(parameterized.TestCase):
81      @parameterized.named_parameters(
82        {'testcase_name': 'Normal',
83          'result': True, 'string': 'aaa', 'prefix': 'aa'},
84        {'testcase_name': 'EmptyPrefix',
85          'result': True, 'string': 'abc', 'prefix': ''},
86        {'testcase_name': 'BothEmpty',
87          'result': True, 'string': '', 'prefix': ''})
88      def testStartsWith(self, prefix, string, result):
89        self.assertEqual(result, string.startswith(prefix))
90
91Named tests also have the benefit that they can be run individually
92from the command line::
93
94    $ testmodule.py NamedExample.testStartsWithNormal
95    .
96    --------------------------------------------------------------------
97    Ran 1 test in 0.000s
98
99    OK
100
101Parameterized Classes
102=====================
103
104If invocation arguments are shared across test methods in a single
105TestCase class, instead of decorating all test methods
106individually, the class itself can be decorated::
107
108    @parameterized.parameters(
109      (1, 2, 3),
110      (4, 5, 9))
111    class ArithmeticTest(parameterized.TestCase):
112      def testAdd(self, arg1, arg2, result):
113        self.assertEqual(arg1 + arg2, result)
114
115      def testSubtract(self, arg1, arg2, result):
116        self.assertEqual(result - arg1, arg2)
117
118Inputs from Iterables
119=====================
120
121If parameters should be shared across several test cases, or are dynamically
122created from other sources, a single non-tuple iterable can be passed into
123the decorator. This iterable will be used to obtain the test cases::
124
125    class AdditionExample(parameterized.TestCase):
126      @parameterized.parameters(
127        c.op1, c.op2, c.result for c in testcases
128      )
129      def testAddition(self, op1, op2, result):
130        self.assertEqual(result, op1 + op2)
131
132
133Single-Argument Test Methods
134============================
135
136If a test method takes only one argument, the single arguments must not be
137wrapped into a tuple::
138
139    class NegativeNumberExample(parameterized.TestCase):
140      @parameterized.parameters(
141        -1, -3, -4, -5
142      )
143      def testIsNegative(self, arg):
144        self.assertTrue(IsNegative(arg))
145
146
147List/tuple as a Single Argument
148===============================
149
150If a test method takes a single argument of a list/tuple, it must be wrapped
151inside a tuple::
152
153    class ZeroSumExample(parameterized.TestCase):
154      @parameterized.parameters(
155        ([-1, 0, 1], ),
156        ([-2, 0, 2], ),
157      )
158      def testSumIsZero(self, arg):
159        self.assertEqual(0, sum(arg))
160
161
162Cartesian product of Parameter Values as Parametrized Test Cases
163================================================================
164
165If required to test method over a cartesian product of parameters,
166`parameterized.product` may be used to facilitate generation of parameters
167test combinations::
168
169    class TestModuloExample(parameterized.TestCase):
170      @parameterized.product(
171          num=[0, 20, 80],
172          modulo=[2, 4],
173          expected=[0]
174      )
175      def testModuloResult(self, num, modulo, expected):
176        self.assertEqual(expected, num % modulo)
177
178This results in 6 test cases being created - one for each combination of the
179parameters. It is also possible to supply sequences of keyword argument dicts
180as elements of the cartesian product::
181
182    @parameterized.product(
183        (dict(num=5, modulo=3, expected=2),
184         dict(num=7, modulo=4, expected=3)),
185        dtype=(int, float)
186    )
187    def testModuloResult(self, num, modulo, expected, dtype):
188      self.assertEqual(expected, dtype(num) % modulo)
189
190This results in 4 test cases being created - for each of the two sets of test
191data (supplied as kwarg dicts) and for each of the two data types (supplied as
192a named parameter). Multiple keyword argument dicts may be supplied if required.
193
194Async Support
195=============
196
197If a test needs to call async functions, it can inherit from both
198parameterized.TestCase and another TestCase that supports async calls, such
199as [asynctest](https://github.com/Martiusweb/asynctest)::
200
201  import asynctest
202
203  class AsyncExample(parameterized.TestCase, asynctest.TestCase):
204    @parameterized.parameters(
205      ('a', 1),
206      ('b', 2),
207    )
208    async def testSomeAsyncFunction(self, arg, expected):
209      actual = await someAsyncFunction(arg)
210      self.assertEqual(actual, expected)
211"""
212
213from collections import abc
214import functools
215import inspect
216import itertools
217import re
218import types
219import unittest
220
221from absl.testing import absltest
222
223
224_ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>')
225_NAMED = object()
226_ARGUMENT_REPR = object()
227_NAMED_DICT_KEY = 'testcase_name'
228
229
230class NoTestsError(Exception):
231  """Raised when parameterized decorators do not generate any tests."""
232
233
234class DuplicateTestNameError(Exception):
235  """Raised when a parameterized test has the same test name multiple times."""
236
237  def __init__(self, test_class_name, new_test_name, original_test_name):
238    super(DuplicateTestNameError, self).__init__(
239        'Duplicate parameterized test name in {}: generated test name {!r} '
240        '(generated from {!r}) already exists. Consider using '
241        'named_parameters() to give your tests unique names and/or renaming '
242        'the conflicting test method.'.format(
243            test_class_name, new_test_name, original_test_name))
244
245
246def _clean_repr(obj):
247  return _ADDR_RE.sub(r'<\1>', repr(obj))
248
249
250def _non_string_or_bytes_iterable(obj):
251  return (isinstance(obj, abc.Iterable) and not isinstance(obj, str) and
252          not isinstance(obj, bytes))
253
254
255def _format_parameter_list(testcase_params):
256  if isinstance(testcase_params, abc.Mapping):
257    return ', '.join('%s=%s' % (argname, _clean_repr(value))
258                     for argname, value in testcase_params.items())
259  elif _non_string_or_bytes_iterable(testcase_params):
260    return ', '.join(map(_clean_repr, testcase_params))
261  else:
262    return _format_parameter_list((testcase_params,))
263
264
265def _async_wrapped(func):
266  @functools.wraps(func)
267  async def wrapper(*args, **kwargs):
268    return await func(*args, **kwargs)
269  return wrapper
270
271
272class _ParameterizedTestIter(object):
273  """Callable and iterable class for producing new test cases."""
274
275  def __init__(self, test_method, testcases, naming_type, original_name=None):
276    """Returns concrete test functions for a test and a list of parameters.
277
278    The naming_type is used to determine the name of the concrete
279    functions as reported by the unittest framework. If naming_type is
280    _FIRST_ARG, the testcases must be tuples, and the first element must
281    have a string representation that is a valid Python identifier.
282
283    Args:
284      test_method: The decorated test method.
285      testcases: (list of tuple/dict) A list of parameter tuples/dicts for
286          individual test invocations.
287      naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR.
288      original_name: The original test method name. When decorated on a test
289          method, None is passed to __init__ and test_method.__name__ is used.
290          Note test_method.__name__ might be different than the original defined
291          test method because of the use of other decorators. A more accurate
292          value is set by TestGeneratorMetaclass.__new__ later.
293    """
294    self._test_method = test_method
295    self.testcases = testcases
296    self._naming_type = naming_type
297    if original_name is None:
298      original_name = test_method.__name__
299    self._original_name = original_name
300    self.__name__ = _ParameterizedTestIter.__name__
301
302  def __call__(self, *args, **kwargs):
303    raise RuntimeError('You appear to be running a parameterized test case '
304                       'without having inherited from parameterized.'
305                       'TestCase. This is bad because none of '
306                       'your test cases are actually being run. You may also '
307                       'be using another decorator before the parameterized '
308                       'one, in which case you should reverse the order.')
309
310  def __iter__(self):
311    test_method = self._test_method
312    naming_type = self._naming_type
313
314    def make_bound_param_test(testcase_params):
315      @functools.wraps(test_method)
316      def bound_param_test(self):
317        if isinstance(testcase_params, abc.Mapping):
318          return test_method(self, **testcase_params)
319        elif _non_string_or_bytes_iterable(testcase_params):
320          return test_method(self, *testcase_params)
321        else:
322          return test_method(self, testcase_params)
323
324      if naming_type is _NAMED:
325        # Signal the metaclass that the name of the test function is unique
326        # and descriptive.
327        bound_param_test.__x_use_name__ = True
328
329        testcase_name = None
330        if isinstance(testcase_params, abc.Mapping):
331          if _NAMED_DICT_KEY not in testcase_params:
332            raise RuntimeError(
333                'Dict for named tests must contain key "%s"' % _NAMED_DICT_KEY)
334          # Create a new dict to avoid modifying the supplied testcase_params.
335          testcase_name = testcase_params[_NAMED_DICT_KEY]
336          testcase_params = {
337              k: v for k, v in testcase_params.items() if k != _NAMED_DICT_KEY
338          }
339        elif _non_string_or_bytes_iterable(testcase_params):
340          if not isinstance(testcase_params[0], str):
341            raise RuntimeError(
342                'The first element of named test parameters is the test name '
343                'suffix and must be a string')
344          testcase_name = testcase_params[0]
345          testcase_params = testcase_params[1:]
346        else:
347          raise RuntimeError(
348              'Named tests must be passed a dict or non-string iterable.')
349
350        test_method_name = self._original_name
351        # Support PEP-8 underscore style for test naming if used.
352        if (test_method_name.startswith('test_')
353            and testcase_name
354            and not testcase_name.startswith('_')):
355          test_method_name += '_'
356
357        bound_param_test.__name__ = test_method_name + str(testcase_name)
358      elif naming_type is _ARGUMENT_REPR:
359        # If it's a generator, convert it to a tuple and treat them as
360        # parameters.
361        if isinstance(testcase_params, types.GeneratorType):
362          testcase_params = tuple(testcase_params)
363        # The metaclass creates a unique, but non-descriptive method name for
364        # _ARGUMENT_REPR tests using an indexed suffix.
365        # To keep test names descriptive, only the original method name is used.
366        # To make sure test names are unique, we add a unique descriptive suffix
367        # __x_params_repr__ for every test.
368        params_repr = '(%s)' % (_format_parameter_list(testcase_params),)
369        bound_param_test.__x_params_repr__ = params_repr
370      else:
371        raise RuntimeError('%s is not a valid naming type.' % (naming_type,))
372
373      bound_param_test.__doc__ = '%s(%s)' % (
374          bound_param_test.__name__, _format_parameter_list(testcase_params))
375      if test_method.__doc__:
376        bound_param_test.__doc__ += '\n%s' % (test_method.__doc__,)
377      if inspect.iscoroutinefunction(test_method):
378        return _async_wrapped(bound_param_test)
379      return bound_param_test
380
381    return (make_bound_param_test(c) for c in self.testcases)
382
383
384def _modify_class(class_object, testcases, naming_type):
385  assert not getattr(class_object, '_test_params_reprs', None), (
386      'Cannot add parameters to %s. Either it already has parameterized '
387      'methods, or its super class is also a parameterized class.' % (
388          class_object,))
389  # NOTE: _test_params_repr is private to parameterized.TestCase and it's
390  # metaclass; do not use it outside of those classes.
391  class_object._test_params_reprs = test_params_reprs = {}
392  for name, obj in class_object.__dict__.copy().items():
393    if (name.startswith(unittest.TestLoader.testMethodPrefix)
394        and isinstance(obj, types.FunctionType)):
395      delattr(class_object, name)
396      methods = {}
397      _update_class_dict_for_param_test_case(
398          class_object.__name__, methods, test_params_reprs, name,
399          _ParameterizedTestIter(obj, testcases, naming_type, name))
400      for meth_name, meth in methods.items():
401        setattr(class_object, meth_name, meth)
402
403
404def _parameter_decorator(naming_type, testcases):
405  """Implementation of the parameterization decorators.
406
407  Args:
408    naming_type: The naming type.
409    testcases: Testcase parameters.
410
411  Raises:
412    NoTestsError: Raised when the decorator generates no tests.
413
414  Returns:
415    A function for modifying the decorated object.
416  """
417  def _apply(obj):
418    if isinstance(obj, type):
419      _modify_class(obj, testcases, naming_type)
420      return obj
421    else:
422      return _ParameterizedTestIter(obj, testcases, naming_type)
423
424  if (len(testcases) == 1 and
425      not isinstance(testcases[0], tuple) and
426      not isinstance(testcases[0], abc.Mapping)):
427    # Support using a single non-tuple parameter as a list of test cases.
428    # Note that the single non-tuple parameter can't be Mapping either, which
429    # means a single dict parameter case.
430    assert _non_string_or_bytes_iterable(testcases[0]), (
431        'Single parameter argument must be a non-string non-Mapping iterable')
432    testcases = testcases[0]
433
434  if not isinstance(testcases, abc.Sequence):
435    testcases = list(testcases)
436  if not testcases:
437    raise NoTestsError(
438        'parameterized test decorators did not generate any tests. '
439        'Make sure you specify non-empty parameters, '
440        'and do not reuse generators more than once.')
441
442  return _apply
443
444
445def parameters(*testcases):
446  """A decorator for creating parameterized tests.
447
448  See the module docstring for a usage example.
449
450  Args:
451    *testcases: Parameters for the decorated method, either a single
452        iterable, or a list of tuples/dicts/objects (for tests with only one
453        argument).
454
455  Raises:
456    NoTestsError: Raised when the decorator generates no tests.
457
458  Returns:
459     A test generator to be handled by TestGeneratorMetaclass.
460  """
461  return _parameter_decorator(_ARGUMENT_REPR, testcases)
462
463
464def named_parameters(*testcases):
465  """A decorator for creating parameterized tests.
466
467  See the module docstring for a usage example. For every parameter tuple
468  passed, the first element of the tuple should be a string and will be appended
469  to the name of the test method. Each parameter dict passed must have a value
470  for the key "testcase_name", the string representation of that value will be
471  appended to the name of the test method.
472
473  Args:
474    *testcases: Parameters for the decorated method, either a single iterable,
475        or a list of tuples or dicts.
476
477  Raises:
478    NoTestsError: Raised when the decorator generates no tests.
479
480  Returns:
481     A test generator to be handled by TestGeneratorMetaclass.
482  """
483  return _parameter_decorator(_NAMED, testcases)
484
485
486def product(*kwargs_seqs, **testgrid):
487  """A decorator for running tests over cartesian product of parameters values.
488
489  See the module docstring for a usage example. The test will be run for every
490  possible combination of the parameters.
491
492  Args:
493    *kwargs_seqs: Each positional parameter is a sequence of keyword arg dicts;
494      every test case generated will include exactly one kwargs dict from each
495      positional parameter; these will then be merged to form an overall list
496      of arguments for the test case.
497    **testgrid: A mapping of parameter names and their possible values. Possible
498      values should given as either a list or a tuple.
499
500  Raises:
501    NoTestsError: Raised when the decorator generates no tests.
502
503  Returns:
504     A test generator to be handled by TestGeneratorMetaclass.
505  """
506
507  for name, values in testgrid.items():
508    assert isinstance(values, (list, tuple)), (
509        'Values of {} must be given as list or tuple, found {}'.format(
510            name, type(values)))
511
512  prior_arg_names = set()
513  for kwargs_seq in kwargs_seqs:
514    assert ((isinstance(kwargs_seq, (list, tuple))) and
515            all(isinstance(kwargs, dict) for kwargs in kwargs_seq)), (
516                'Positional parameters must be a sequence of keyword arg'
517                'dicts, found {}'
518                .format(kwargs_seq))
519    if kwargs_seq:
520      arg_names = set(kwargs_seq[0])
521      assert all(set(kwargs) == arg_names for kwargs in kwargs_seq), (
522          'Keyword argument dicts within a single parameter must all have the '
523          'same keys, found {}'.format(kwargs_seq))
524      assert not (arg_names & prior_arg_names), (
525          'Keyword argument dict sequences must all have distinct argument '
526          'names, found duplicate(s) {}'
527          .format(sorted(arg_names & prior_arg_names)))
528      prior_arg_names |= arg_names
529
530  assert not (prior_arg_names & set(testgrid)), (
531      'Arguments supplied in kwargs dicts in positional parameters must not '
532      'overlap with arguments supplied as named parameters; found duplicate '
533      'argument(s) {}'.format(sorted(prior_arg_names & set(testgrid))))
534
535  # Convert testgrid into a sequence of sequences of kwargs dicts and combine
536  # with the positional parameters.
537  # So foo=[1,2], bar=[3,4] --> [[{foo: 1}, {foo: 2}], [{bar: 3, bar: 4}]]
538  testgrid = (tuple({k: v} for v in vs) for k, vs in testgrid.items())
539  testgrid = tuple(kwargs_seqs) + tuple(testgrid)
540
541  # Create all possible combinations of parameters as a cartesian product
542  # of parameter values.
543  testcases = [
544      dict(itertools.chain.from_iterable(case.items()
545                                         for case in cases))
546      for cases in itertools.product(*testgrid)
547  ]
548  return _parameter_decorator(_ARGUMENT_REPR, testcases)
549
550
551class TestGeneratorMetaclass(type):
552  """Metaclass for adding tests generated by parameterized decorators."""
553
554  def __new__(cls, class_name, bases, dct):
555    # NOTE: _test_params_repr is private to parameterized.TestCase and it's
556    # metaclass; do not use it outside of those classes.
557    test_params_reprs = dct.setdefault('_test_params_reprs', {})
558    for name, obj in dct.copy().items():
559      if (name.startswith(unittest.TestLoader.testMethodPrefix) and
560          _non_string_or_bytes_iterable(obj)):
561        # NOTE: `obj` might not be a _ParameterizedTestIter in two cases:
562        # 1. a class-level iterable named test* that isn't a test, such as
563        #    a list of something. Such attributes get deleted from the class.
564        #
565        # 2. If a decorator is applied to the parameterized test, e.g.
566        #    @morestuff
567        #    @parameterized.parameters(...)
568        #    def test_foo(...): ...
569        #
570        #   This is OK so long as the underlying parameterized function state
571        #   is forwarded (e.g. using functool.wraps() and **without**
572        #   accessing explicitly accessing the internal attributes.
573        if isinstance(obj, _ParameterizedTestIter):
574          # Update the original test method name so it's more accurate.
575          # The mismatch might happen when another decorator is used inside
576          # the parameterized decrators, and the inner decorator doesn't
577          # preserve its __name__.
578          obj._original_name = name
579        iterator = iter(obj)
580        dct.pop(name)
581        _update_class_dict_for_param_test_case(
582            class_name, dct, test_params_reprs, name, iterator)
583    # If the base class is a subclass of parameterized.TestCase, inherit its
584    # _test_params_reprs too.
585    for base in bases:
586      # Check if the base has _test_params_reprs first, then check if it's a
587      # subclass of parameterized.TestCase. Otherwise when this is called for
588      # the parameterized.TestCase definition itself, this raises because
589      # itself is not defined yet. This works as long as absltest.TestCase does
590      # not define _test_params_reprs.
591      base_test_params_reprs = getattr(base, '_test_params_reprs', None)
592      if base_test_params_reprs and issubclass(base, TestCase):
593        for test_method, test_method_id in base_test_params_reprs.items():
594          # test_method may both exists in base and this class.
595          # This class's method overrides base class's.
596          # That's why it should only inherit it if it does not exist.
597          test_params_reprs.setdefault(test_method, test_method_id)
598
599    return type.__new__(cls, class_name, bases, dct)
600
601
602def _update_class_dict_for_param_test_case(
603    test_class_name, dct, test_params_reprs, name, iterator):
604  """Adds individual test cases to a dictionary.
605
606  Args:
607    test_class_name: The name of the class tests are added to.
608    dct: The target dictionary.
609    test_params_reprs: The dictionary for mapping names to test IDs.
610    name: The original name of the test case.
611    iterator: The iterator generating the individual test cases.
612
613  Raises:
614    DuplicateTestNameError: Raised when a test name occurs multiple times.
615    RuntimeError: If non-parameterized functions are generated.
616  """
617  for idx, func in enumerate(iterator):
618    assert callable(func), 'Test generators must yield callables, got %r' % (
619        func,)
620    if not (getattr(func, '__x_use_name__', None) or
621            getattr(func, '__x_params_repr__', None)):
622      raise RuntimeError(
623          '{}.{} generated a test function without using the parameterized '
624          'decorators. Only tests generated using the decorators are '
625          'supported.'.format(test_class_name, name))
626
627    if getattr(func, '__x_use_name__', False):
628      original_name = func.__name__
629      new_name = original_name
630    else:
631      original_name = name
632      new_name = '%s%d' % (original_name, idx)
633
634    if new_name in dct:
635      raise DuplicateTestNameError(test_class_name, new_name, original_name)
636
637    dct[new_name] = func
638    test_params_reprs[new_name] = getattr(func, '__x_params_repr__', '')
639
640
641class TestCase(absltest.TestCase, metaclass=TestGeneratorMetaclass):
642  """Base class for test cases using the parameters decorator."""
643
644  # visibility: private; do not call outside this class.
645  def _get_params_repr(self):
646    return self._test_params_reprs.get(self._testMethodName, '')
647
648  def __str__(self):
649    params_repr = self._get_params_repr()
650    if params_repr:
651      params_repr = ' ' + params_repr
652    return '{}{} ({})'.format(
653        self._testMethodName, params_repr,
654        unittest.util.strclass(self.__class__))
655
656  def id(self):
657    """Returns the descriptive ID of the test.
658
659    This is used internally by the unittesting framework to get a name
660    for the test to be used in reports.
661
662    Returns:
663      The test id.
664    """
665    base = super(TestCase, self).id()
666    params_repr = self._get_params_repr()
667    if params_repr:
668      # We include the params in the id so that, when reported in the
669      # test.xml file, the value is more informative than just "test_foo0".
670      # Use a space to separate them so that it's copy/paste friendly and
671      # easy to identify the actual test id.
672      return '{} {}'.format(base, params_repr)
673    else:
674      return base
675
676
677# This function is kept CamelCase because it's used as a class's base class.
678def CoopTestCase(other_base_class):  # pylint: disable=invalid-name
679  """Returns a new base class with a cooperative metaclass base.
680
681  This enables the TestCase to be used in combination
682  with other base classes that have custom metaclasses, such as
683  ``mox.MoxTestBase``.
684
685  Only works with metaclasses that do not override ``type.__new__``.
686
687  Example::
688
689      from absl.testing import parameterized
690
691      class ExampleTest(parameterized.CoopTestCase(OtherTestCase)):
692        ...
693
694  Args:
695    other_base_class: (class) A test case base class.
696
697  Returns:
698    A new class object.
699  """
700  metaclass = type(
701      'CoopMetaclass',
702      (other_base_class.__metaclass__,
703       TestGeneratorMetaclass), {})
704  return metaclass(
705      'CoopTestCase',
706      (other_base_class, TestCase), {})
707