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 Parameterized 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 220import warnings 221 222from absl.testing import absltest 223 224 225_ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') 226_NAMED = object() 227_ARGUMENT_REPR = object() 228_NAMED_DICT_KEY = 'testcase_name' 229 230 231class NoTestsError(Exception): 232 """Raised when parameterized decorators do not generate any tests.""" 233 234 235class DuplicateTestNameError(Exception): 236 """Raised when a parameterized test has the same test name multiple times.""" 237 238 def __init__(self, test_class_name, new_test_name, original_test_name): 239 super(DuplicateTestNameError, self).__init__( 240 'Duplicate parameterized test name in {}: generated test name {!r} ' 241 '(generated from {!r}) already exists. Consider using ' 242 'named_parameters() to give your tests unique names and/or renaming ' 243 'the conflicting test method.'.format( 244 test_class_name, new_test_name, original_test_name)) 245 246 247def _clean_repr(obj): 248 return _ADDR_RE.sub(r'<\1>', repr(obj)) 249 250 251def _non_string_or_bytes_iterable(obj): 252 return (isinstance(obj, abc.Iterable) and not isinstance(obj, str) and 253 not isinstance(obj, bytes)) 254 255 256def _format_parameter_list(testcase_params): 257 if isinstance(testcase_params, abc.Mapping): 258 return ', '.join('%s=%s' % (argname, _clean_repr(value)) 259 for argname, value in testcase_params.items()) 260 elif _non_string_or_bytes_iterable(testcase_params): 261 return ', '.join(map(_clean_repr, testcase_params)) 262 else: 263 return _format_parameter_list((testcase_params,)) 264 265 266def _async_wrapped(func): 267 @functools.wraps(func) 268 async def wrapper(*args, **kwargs): 269 return await func(*args, **kwargs) 270 return wrapper 271 272 273class _ParameterizedTestIter(object): 274 """Callable and iterable class for producing new test cases.""" 275 276 def __init__(self, test_method, testcases, naming_type, original_name=None): 277 """Returns concrete test functions for a test and a list of parameters. 278 279 The naming_type is used to determine the name of the concrete 280 functions as reported by the unittest framework. If naming_type is 281 _FIRST_ARG, the testcases must be tuples, and the first element must 282 have a string representation that is a valid Python identifier. 283 284 Args: 285 test_method: The decorated test method. 286 testcases: (list of tuple/dict) A list of parameter tuples/dicts for 287 individual test invocations. 288 naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. 289 original_name: The original test method name. When decorated on a test 290 method, None is passed to __init__ and test_method.__name__ is used. 291 Note test_method.__name__ might be different than the original defined 292 test method because of the use of other decorators. A more accurate 293 value is set by TestGeneratorMetaclass.__new__ later. 294 """ 295 self._test_method = test_method 296 self.testcases = testcases 297 self._naming_type = naming_type 298 if original_name is None: 299 original_name = test_method.__name__ 300 self._original_name = original_name 301 self.__name__ = _ParameterizedTestIter.__name__ 302 303 def __call__(self, *args, **kwargs): 304 raise RuntimeError('You appear to be running a parameterized test case ' 305 'without having inherited from parameterized.' 306 'TestCase. This is bad because none of ' 307 'your test cases are actually being run. You may also ' 308 'be using another decorator before the parameterized ' 309 'one, in which case you should reverse the order.') 310 311 def __iter__(self): 312 test_method = self._test_method 313 naming_type = self._naming_type 314 315 def make_bound_param_test(testcase_params): 316 @functools.wraps(test_method) 317 def bound_param_test(self): 318 if isinstance(testcase_params, abc.Mapping): 319 return test_method(self, **testcase_params) 320 elif _non_string_or_bytes_iterable(testcase_params): 321 return test_method(self, *testcase_params) 322 else: 323 return test_method(self, testcase_params) 324 325 if naming_type is _NAMED: 326 # Signal the metaclass that the name of the test function is unique 327 # and descriptive. 328 bound_param_test.__x_use_name__ = True 329 330 testcase_name = None 331 if isinstance(testcase_params, abc.Mapping): 332 if _NAMED_DICT_KEY not in testcase_params: 333 raise RuntimeError( 334 'Dict for named tests must contain key "%s"' % _NAMED_DICT_KEY) 335 # Create a new dict to avoid modifying the supplied testcase_params. 336 testcase_name = testcase_params[_NAMED_DICT_KEY] 337 testcase_params = { 338 k: v for k, v in testcase_params.items() if k != _NAMED_DICT_KEY 339 } 340 elif _non_string_or_bytes_iterable(testcase_params): 341 if not isinstance(testcase_params[0], str): 342 raise RuntimeError( 343 'The first element of named test parameters is the test name ' 344 'suffix and must be a string') 345 testcase_name = testcase_params[0] 346 testcase_params = testcase_params[1:] 347 else: 348 raise RuntimeError( 349 'Named tests must be passed a dict or non-string iterable.') 350 351 test_method_name = self._original_name 352 # Support PEP-8 underscore style for test naming if used. 353 if (test_method_name.startswith('test_') 354 and testcase_name 355 and not testcase_name.startswith('_')): 356 test_method_name += '_' 357 358 bound_param_test.__name__ = test_method_name + str(testcase_name) 359 elif naming_type is _ARGUMENT_REPR: 360 # If it's a generator, convert it to a tuple and treat them as 361 # parameters. 362 if isinstance(testcase_params, types.GeneratorType): 363 testcase_params = tuple(testcase_params) 364 # The metaclass creates a unique, but non-descriptive method name for 365 # _ARGUMENT_REPR tests using an indexed suffix. 366 # To keep test names descriptive, only the original method name is used. 367 # To make sure test names are unique, we add a unique descriptive suffix 368 # __x_params_repr__ for every test. 369 params_repr = '(%s)' % (_format_parameter_list(testcase_params),) 370 bound_param_test.__x_params_repr__ = params_repr 371 else: 372 raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) 373 374 bound_param_test.__doc__ = '%s(%s)' % ( 375 bound_param_test.__name__, _format_parameter_list(testcase_params)) 376 if test_method.__doc__: 377 bound_param_test.__doc__ += '\n%s' % (test_method.__doc__,) 378 if inspect.iscoroutinefunction(test_method): 379 return _async_wrapped(bound_param_test) 380 return bound_param_test 381 382 return (make_bound_param_test(c) for c in self.testcases) 383 384 385def _modify_class(class_object, testcases, naming_type): 386 assert not getattr(class_object, '_test_params_reprs', None), ( 387 'Cannot add parameters to %s. Either it already has parameterized ' 388 'methods, or its super class is also a parameterized class.' % ( 389 class_object,)) 390 # NOTE: _test_params_repr is private to parameterized.TestCase and it's 391 # metaclass; do not use it outside of those classes. 392 class_object._test_params_reprs = test_params_reprs = {} 393 for name, obj in class_object.__dict__.copy().items(): 394 if (name.startswith(unittest.TestLoader.testMethodPrefix) 395 and isinstance(obj, types.FunctionType)): 396 delattr(class_object, name) 397 methods = {} 398 _update_class_dict_for_param_test_case( 399 class_object.__name__, methods, test_params_reprs, name, 400 _ParameterizedTestIter(obj, testcases, naming_type, name)) 401 for meth_name, meth in methods.items(): 402 setattr(class_object, meth_name, meth) 403 404 405def _parameter_decorator(naming_type, testcases): 406 """Implementation of the parameterization decorators. 407 408 Args: 409 naming_type: The naming type. 410 testcases: Testcase parameters. 411 412 Raises: 413 NoTestsError: Raised when the decorator generates no tests. 414 415 Returns: 416 A function for modifying the decorated object. 417 """ 418 def _apply(obj): 419 if isinstance(obj, type): 420 _modify_class(obj, testcases, naming_type) 421 return obj 422 else: 423 return _ParameterizedTestIter(obj, testcases, naming_type) 424 425 if (len(testcases) == 1 and 426 not isinstance(testcases[0], tuple) and 427 not isinstance(testcases[0], abc.Mapping)): 428 # Support using a single non-tuple parameter as a list of test cases. 429 # Note that the single non-tuple parameter can't be Mapping either, which 430 # means a single dict parameter case. 431 assert _non_string_or_bytes_iterable(testcases[0]), ( 432 'Single parameter argument must be a non-string non-Mapping iterable') 433 testcases = testcases[0] 434 435 if not isinstance(testcases, abc.Sequence): 436 testcases = list(testcases) 437 if not testcases: 438 raise NoTestsError( 439 'parameterized test decorators did not generate any tests. ' 440 'Make sure you specify non-empty parameters, ' 441 'and do not reuse generators more than once.') 442 443 return _apply 444 445 446def parameters(*testcases): 447 """A decorator for creating parameterized tests. 448 449 See the module docstring for a usage example. 450 451 Args: 452 *testcases: Parameters for the decorated method, either a single 453 iterable, or a list of tuples/dicts/objects (for tests with only one 454 argument). 455 456 Raises: 457 NoTestsError: Raised when the decorator generates no tests. 458 459 Returns: 460 A test generator to be handled by TestGeneratorMetaclass. 461 """ 462 return _parameter_decorator(_ARGUMENT_REPR, testcases) 463 464 465def named_parameters(*testcases): 466 """A decorator for creating parameterized tests. 467 468 See the module docstring for a usage example. For every parameter tuple 469 passed, the first element of the tuple should be a string and will be appended 470 to the name of the test method. Each parameter dict passed must have a value 471 for the key "testcase_name", the string representation of that value will be 472 appended to the name of the test method. 473 474 Args: 475 *testcases: Parameters for the decorated method, either a single iterable, 476 or a list of tuples or dicts. 477 478 Raises: 479 NoTestsError: Raised when the decorator generates no tests. 480 481 Returns: 482 A test generator to be handled by TestGeneratorMetaclass. 483 """ 484 return _parameter_decorator(_NAMED, testcases) 485 486 487def product(*kwargs_seqs, **testgrid): 488 """A decorator for running tests over cartesian product of parameters values. 489 490 See the module docstring for a usage example. The test will be run for every 491 possible combination of the parameters. 492 493 Args: 494 *kwargs_seqs: Each positional parameter is a sequence of keyword arg dicts; 495 every test case generated will include exactly one kwargs dict from each 496 positional parameter; these will then be merged to form an overall list 497 of arguments for the test case. 498 **testgrid: A mapping of parameter names and their possible values. Possible 499 values should given as either a list or a tuple. 500 501 Raises: 502 NoTestsError: Raised when the decorator generates no tests. 503 504 Returns: 505 A test generator to be handled by TestGeneratorMetaclass. 506 """ 507 508 for name, values in testgrid.items(): 509 assert isinstance(values, (list, tuple)), ( 510 'Values of {} must be given as list or tuple, found {}'.format( 511 name, type(values))) 512 513 prior_arg_names = set() 514 for kwargs_seq in kwargs_seqs: 515 assert ((isinstance(kwargs_seq, (list, tuple))) and 516 all(isinstance(kwargs, dict) for kwargs in kwargs_seq)), ( 517 'Positional parameters must be a sequence of keyword arg' 518 'dicts, found {}' 519 .format(kwargs_seq)) 520 if kwargs_seq: 521 arg_names = set(kwargs_seq[0]) 522 assert all(set(kwargs) == arg_names for kwargs in kwargs_seq), ( 523 'Keyword argument dicts within a single parameter must all have the ' 524 'same keys, found {}'.format(kwargs_seq)) 525 assert not (arg_names & prior_arg_names), ( 526 'Keyword argument dict sequences must all have distinct argument ' 527 'names, found duplicate(s) {}' 528 .format(sorted(arg_names & prior_arg_names))) 529 prior_arg_names |= arg_names 530 531 assert not (prior_arg_names & set(testgrid)), ( 532 'Arguments supplied in kwargs dicts in positional parameters must not ' 533 'overlap with arguments supplied as named parameters; found duplicate ' 534 'argument(s) {}'.format(sorted(prior_arg_names & set(testgrid)))) 535 536 # Convert testgrid into a sequence of sequences of kwargs dicts and combine 537 # with the positional parameters. 538 # So foo=[1,2], bar=[3,4] --> [[{foo: 1}, {foo: 2}], [{bar: 3, bar: 4}]] 539 testgrid = (tuple({k: v} for v in vs) for k, vs in testgrid.items()) 540 testgrid = tuple(kwargs_seqs) + tuple(testgrid) 541 542 # Create all possible combinations of parameters as a cartesian product 543 # of parameter values. 544 testcases = [ 545 dict(itertools.chain.from_iterable(case.items() 546 for case in cases)) 547 for cases in itertools.product(*testgrid) 548 ] 549 return _parameter_decorator(_ARGUMENT_REPR, testcases) 550 551 552class TestGeneratorMetaclass(type): 553 """Metaclass for adding tests generated by parameterized decorators.""" 554 555 def __new__(cls, class_name, bases, dct): 556 # NOTE: _test_params_repr is private to parameterized.TestCase and it's 557 # metaclass; do not use it outside of those classes. 558 test_params_reprs = dct.setdefault('_test_params_reprs', {}) 559 for name, obj in dct.copy().items(): 560 if (name.startswith(unittest.TestLoader.testMethodPrefix) and 561 _non_string_or_bytes_iterable(obj)): 562 # NOTE: `obj` might not be a _ParameterizedTestIter in two cases: 563 # 1. a class-level iterable named test* that isn't a test, such as 564 # a list of something. Such attributes get deleted from the class. 565 # 566 # 2. If a decorator is applied to the parameterized test, e.g. 567 # @morestuff 568 # @parameterized.parameters(...) 569 # def test_foo(...): ... 570 # 571 # This is OK so long as the underlying parameterized function state 572 # is forwarded (e.g. using functool.wraps() and **without** 573 # accessing explicitly accessing the internal attributes. 574 if isinstance(obj, _ParameterizedTestIter): 575 # Update the original test method name so it's more accurate. 576 # The mismatch might happen when another decorator is used inside 577 # the parameterized decrators, and the inner decorator doesn't 578 # preserve its __name__. 579 obj._original_name = name 580 iterator = iter(obj) 581 dct.pop(name) 582 _update_class_dict_for_param_test_case( 583 class_name, dct, test_params_reprs, name, iterator) 584 # If the base class is a subclass of parameterized.TestCase, inherit its 585 # _test_params_reprs too. 586 for base in bases: 587 # Check if the base has _test_params_reprs first, then check if it's a 588 # subclass of parameterized.TestCase. Otherwise when this is called for 589 # the parameterized.TestCase definition itself, this raises because 590 # itself is not defined yet. This works as long as absltest.TestCase does 591 # not define _test_params_reprs. 592 base_test_params_reprs = getattr(base, '_test_params_reprs', None) 593 if base_test_params_reprs and issubclass(base, TestCase): 594 for test_method, test_method_id in base_test_params_reprs.items(): 595 # test_method may both exists in base and this class. 596 # This class's method overrides base class's. 597 # That's why it should only inherit it if it does not exist. 598 test_params_reprs.setdefault(test_method, test_method_id) 599 600 return type.__new__(cls, class_name, bases, dct) 601 602 603def _update_class_dict_for_param_test_case( 604 test_class_name, dct, test_params_reprs, name, iterator): 605 """Adds individual test cases to a dictionary. 606 607 Args: 608 test_class_name: The name of the class tests are added to. 609 dct: The target dictionary. 610 test_params_reprs: The dictionary for mapping names to test IDs. 611 name: The original name of the test case. 612 iterator: The iterator generating the individual test cases. 613 614 Raises: 615 DuplicateTestNameError: Raised when a test name occurs multiple times. 616 RuntimeError: If non-parameterized functions are generated. 617 """ 618 for idx, func in enumerate(iterator): 619 assert callable(func), 'Test generators must yield callables, got %r' % ( 620 func,) 621 if not (getattr(func, '__x_use_name__', None) or 622 getattr(func, '__x_params_repr__', None)): 623 raise RuntimeError( 624 '{}.{} generated a test function without using the parameterized ' 625 'decorators. Only tests generated using the decorators are ' 626 'supported.'.format(test_class_name, name)) 627 628 if getattr(func, '__x_use_name__', False): 629 original_name = func.__name__ 630 new_name = original_name 631 else: 632 original_name = name 633 new_name = '%s%d' % (original_name, idx) 634 635 if new_name in dct: 636 raise DuplicateTestNameError(test_class_name, new_name, original_name) 637 638 dct[new_name] = func 639 test_params_reprs[new_name] = getattr(func, '__x_params_repr__', '') 640 641 642class TestCase(absltest.TestCase, metaclass=TestGeneratorMetaclass): 643 """Base class for test cases using the parameters decorator.""" 644 645 # visibility: private; do not call outside this class. 646 def _get_params_repr(self): 647 return self._test_params_reprs.get(self._testMethodName, '') 648 649 def __str__(self): 650 params_repr = self._get_params_repr() 651 if params_repr: 652 params_repr = ' ' + params_repr 653 return '{}{} ({})'.format( 654 self._testMethodName, params_repr, 655 unittest.util.strclass(self.__class__)) 656 657 def id(self): 658 """Returns the descriptive ID of the test. 659 660 This is used internally by the unittesting framework to get a name 661 for the test to be used in reports. 662 663 Returns: 664 The test id. 665 """ 666 base = super(TestCase, self).id() 667 params_repr = self._get_params_repr() 668 if params_repr: 669 # We include the params in the id so that, when reported in the 670 # test.xml file, the value is more informative than just "test_foo0". 671 # Use a space to separate them so that it's copy/paste friendly and 672 # easy to identify the actual test id. 673 return '{} {}'.format(base, params_repr) 674 else: 675 return base 676 677 678# This function is kept CamelCase because it's used as a class's base class. 679def CoopTestCase(other_base_class): # pylint: disable=invalid-name 680 """Returns a new base class with a cooperative metaclass base. 681 682 This enables the TestCase to be used in combination 683 with other base classes that have custom metaclasses, such as 684 ``mox.MoxTestBase``. 685 686 Only works with metaclasses that do not override ``type.__new__``. 687 688 Example:: 689 690 from absl.testing import parameterized 691 692 class ExampleTest(parameterized.CoopTestCase(OtherTestCase)): 693 ... 694 695 Args: 696 other_base_class: (class) A test case base class. 697 698 Returns: 699 A new class object. 700 """ 701 # If the other base class has a metaclass of 'type' then trying to combine 702 # the metaclasses will result in an MRO error. So simply combine them and 703 # return. 704 if type(other_base_class) == type: # pylint: disable=unidiomatic-typecheck 705 warnings.warn( 706 'CoopTestCase is only necessary when combining with a class that uses' 707 ' a metaclass. Use multiple inheritance like this instead: class' 708 f' ExampleTest(paramaterized.TestCase, {other_base_class.__name__}):', 709 stacklevel=2, 710 ) 711 712 class CoopTestCaseBase(other_base_class, TestCase): 713 pass 714 715 return CoopTestCaseBase 716 else: 717 718 class CoopMetaclass(type(other_base_class), TestGeneratorMetaclass): # pylint: disable=unused-variable 719 pass 720 721 class CoopTestCaseBase(other_base_class, TestCase, metaclass=CoopMetaclass): 722 pass 723 724 return CoopTestCaseBase 725