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