1#! /usr/bin/env python 2# 3# Protocol Buffers - Google's data interchange format 4# Copyright 2008 Google Inc. All rights reserved. 5# https://developers.google.com/protocol-buffers/ 6# 7# Redistribution and use in source and binary forms, with or without 8# modification, are permitted provided that the following conditions are 9# met: 10# 11# * Redistributions of source code must retain the above copyright 12# notice, this list of conditions and the following disclaimer. 13# * Redistributions in binary form must reproduce the above 14# copyright notice, this list of conditions and the following disclaimer 15# in the documentation and/or other materials provided with the 16# distribution. 17# * Neither the name of Google Inc. nor the names of its 18# contributors may be used to endorse or promote products derived from 19# this software without specific prior written permission. 20# 21# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 33"""Adds support for parameterized tests to Python's unittest TestCase class. 34 35A parameterized test is a method in a test case that is invoked with different 36argument tuples. 37 38A simple example: 39 40 class AdditionExample(parameterized.TestCase): 41 @parameterized.parameters( 42 (1, 2, 3), 43 (4, 5, 9), 44 (1, 1, 3)) 45 def testAddition(self, op1, op2, result): 46 self.assertEqual(result, op1 + op2) 47 48 49Each invocation is a separate test case and properly isolated just 50like a normal test method, with its own setUp/tearDown cycle. In the 51example above, there are three separate testcases, one of which will 52fail due to an assertion error (1 + 1 != 3). 53 54Parameters for invididual test cases can be tuples (with positional parameters) 55or dictionaries (with named parameters): 56 57 class AdditionExample(parameterized.TestCase): 58 @parameterized.parameters( 59 {'op1': 1, 'op2': 2, 'result': 3}, 60 {'op1': 4, 'op2': 5, 'result': 9}, 61 ) 62 def testAddition(self, op1, op2, result): 63 self.assertEqual(result, op1 + op2) 64 65If a parameterized test fails, the error message will show the 66original test name (which is modified internally) and the arguments 67for the specific invocation, which are part of the string returned by 68the shortDescription() method on test cases. 69 70The id method of the test, used internally by the unittest framework, 71is also modified to show the arguments. To make sure that test names 72stay the same across several invocations, object representations like 73 74 >>> class Foo(object): 75 ... pass 76 >>> repr(Foo()) 77 '<__main__.Foo object at 0x23d8610>' 78 79are turned into '<__main__.Foo>'. For even more descriptive names, 80especially in test logs, you can use the named_parameters decorator. In 81this case, only tuples are supported, and the first parameters has to 82be a string (or an object that returns an apt name when converted via 83str()): 84 85 class NamedExample(parameterized.TestCase): 86 @parameterized.named_parameters( 87 ('Normal', 'aa', 'aaa', True), 88 ('EmptyPrefix', '', 'abc', True), 89 ('BothEmpty', '', '', True)) 90 def testStartsWith(self, prefix, string, result): 91 self.assertEqual(result, strings.startswith(prefix)) 92 93Named tests also have the benefit that they can be run individually 94from the command line: 95 96 $ testmodule.py NamedExample.testStartsWithNormal 97 . 98 -------------------------------------------------------------------- 99 Ran 1 test in 0.000s 100 101 OK 102 103Parameterized Classes 104===================== 105If invocation arguments are shared across test methods in a single 106TestCase class, instead of decorating all test methods 107individually, the class itself can be decorated: 108 109 @parameterized.parameters( 110 (1, 2, 3) 111 (4, 5, 9)) 112 class ArithmeticTest(parameterized.TestCase): 113 def testAdd(self, arg1, arg2, result): 114 self.assertEqual(arg1 + arg2, result) 115 116 def testSubtract(self, arg2, arg2, result): 117 self.assertEqual(result - arg1, arg2) 118 119Inputs from Iterables 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============================ 135If a test method takes only one argument, the single argument does not need to 136be wrapped into a tuple: 137 138 class NegativeNumberExample(parameterized.TestCase): 139 @parameterized.parameters( 140 -1, -3, -4, -5 141 ) 142 def testIsNegative(self, arg): 143 self.assertTrue(IsNegative(arg)) 144""" 145 146__author__ = 'tmarek@google.com (Torsten Marek)' 147 148import functools 149import re 150import types 151try: 152 import unittest2 as unittest 153except ImportError: 154 import unittest 155import uuid 156 157import six 158 159try: 160 # Since python 3 161 import collections.abc as collections_abc 162except ImportError: 163 # Won't work after python 3.8 164 import collections as collections_abc 165 166ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') 167_SEPARATOR = uuid.uuid1().hex 168_FIRST_ARG = object() 169_ARGUMENT_REPR = object() 170 171 172def _CleanRepr(obj): 173 return ADDR_RE.sub(r'<\1>', repr(obj)) 174 175 176# Helper function formerly from the unittest module, removed from it in 177# Python 2.7. 178def _StrClass(cls): 179 return '%s.%s' % (cls.__module__, cls.__name__) 180 181 182def _NonStringIterable(obj): 183 return (isinstance(obj, collections_abc.Iterable) and not 184 isinstance(obj, six.string_types)) 185 186 187def _FormatParameterList(testcase_params): 188 if isinstance(testcase_params, collections_abc.Mapping): 189 return ', '.join('%s=%s' % (argname, _CleanRepr(value)) 190 for argname, value in testcase_params.items()) 191 elif _NonStringIterable(testcase_params): 192 return ', '.join(map(_CleanRepr, testcase_params)) 193 else: 194 return _FormatParameterList((testcase_params,)) 195 196 197class _ParameterizedTestIter(object): 198 """Callable and iterable class for producing new test cases.""" 199 200 def __init__(self, test_method, testcases, naming_type): 201 """Returns concrete test functions for a test and a list of parameters. 202 203 The naming_type is used to determine the name of the concrete 204 functions as reported by the unittest framework. If naming_type is 205 _FIRST_ARG, the testcases must be tuples, and the first element must 206 have a string representation that is a valid Python identifier. 207 208 Args: 209 test_method: The decorated test method. 210 testcases: (list of tuple/dict) A list of parameter 211 tuples/dicts for individual test invocations. 212 naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. 213 """ 214 self._test_method = test_method 215 self.testcases = testcases 216 self._naming_type = naming_type 217 218 def __call__(self, *args, **kwargs): 219 raise RuntimeError('You appear to be running a parameterized test case ' 220 'without having inherited from parameterized.' 221 'TestCase. This is bad because none of ' 222 'your test cases are actually being run.') 223 224 def __iter__(self): 225 test_method = self._test_method 226 naming_type = self._naming_type 227 228 def MakeBoundParamTest(testcase_params): 229 @functools.wraps(test_method) 230 def BoundParamTest(self): 231 if isinstance(testcase_params, collections_abc.Mapping): 232 test_method(self, **testcase_params) 233 elif _NonStringIterable(testcase_params): 234 test_method(self, *testcase_params) 235 else: 236 test_method(self, testcase_params) 237 238 if naming_type is _FIRST_ARG: 239 # Signal the metaclass that the name of the test function is unique 240 # and descriptive. 241 BoundParamTest.__x_use_name__ = True 242 BoundParamTest.__name__ += str(testcase_params[0]) 243 testcase_params = testcase_params[1:] 244 elif naming_type is _ARGUMENT_REPR: 245 # __x_extra_id__ is used to pass naming information to the __new__ 246 # method of TestGeneratorMetaclass. 247 # The metaclass will make sure to create a unique, but nondescriptive 248 # name for this test. 249 BoundParamTest.__x_extra_id__ = '(%s)' % ( 250 _FormatParameterList(testcase_params),) 251 else: 252 raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) 253 254 BoundParamTest.__doc__ = '%s(%s)' % ( 255 BoundParamTest.__name__, _FormatParameterList(testcase_params)) 256 if test_method.__doc__: 257 BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,) 258 return BoundParamTest 259 return (MakeBoundParamTest(c) for c in self.testcases) 260 261 262def _IsSingletonList(testcases): 263 """True iff testcases contains only a single non-tuple element.""" 264 return len(testcases) == 1 and not isinstance(testcases[0], tuple) 265 266 267def _ModifyClass(class_object, testcases, naming_type): 268 assert not getattr(class_object, '_id_suffix', None), ( 269 'Cannot add parameters to %s,' 270 ' which already has parameterized methods.' % (class_object,)) 271 class_object._id_suffix = id_suffix = {} 272 # We change the size of __dict__ while we iterate over it, 273 # which Python 3.x will complain about, so use copy(). 274 for name, obj in class_object.__dict__.copy().items(): 275 if (name.startswith(unittest.TestLoader.testMethodPrefix) 276 and isinstance(obj, types.FunctionType)): 277 delattr(class_object, name) 278 methods = {} 279 _UpdateClassDictForParamTestCase( 280 methods, id_suffix, name, 281 _ParameterizedTestIter(obj, testcases, naming_type)) 282 for name, meth in methods.items(): 283 setattr(class_object, name, meth) 284 285 286def _ParameterDecorator(naming_type, testcases): 287 """Implementation of the parameterization decorators. 288 289 Args: 290 naming_type: The naming type. 291 testcases: Testcase parameters. 292 293 Returns: 294 A function for modifying the decorated object. 295 """ 296 def _Apply(obj): 297 if isinstance(obj, type): 298 _ModifyClass( 299 obj, 300 list(testcases) if not isinstance(testcases, collections_abc.Sequence) 301 else testcases, 302 naming_type) 303 return obj 304 else: 305 return _ParameterizedTestIter(obj, testcases, naming_type) 306 307 if _IsSingletonList(testcases): 308 assert _NonStringIterable(testcases[0]), ( 309 'Single parameter argument must be a non-string iterable') 310 testcases = testcases[0] 311 312 return _Apply 313 314 315def parameters(*testcases): # pylint: disable=invalid-name 316 """A decorator for creating parameterized tests. 317 318 See the module docstring for a usage example. 319 Args: 320 *testcases: Parameters for the decorated method, either a single 321 iterable, or a list of tuples/dicts/objects (for tests 322 with only one argument). 323 324 Returns: 325 A test generator to be handled by TestGeneratorMetaclass. 326 """ 327 return _ParameterDecorator(_ARGUMENT_REPR, testcases) 328 329 330def named_parameters(*testcases): # pylint: disable=invalid-name 331 """A decorator for creating parameterized tests. 332 333 See the module docstring for a usage example. The first element of 334 each parameter tuple should be a string and will be appended to the 335 name of the test method. 336 337 Args: 338 *testcases: Parameters for the decorated method, either a single 339 iterable, or a list of tuples. 340 341 Returns: 342 A test generator to be handled by TestGeneratorMetaclass. 343 """ 344 return _ParameterDecorator(_FIRST_ARG, testcases) 345 346 347class TestGeneratorMetaclass(type): 348 """Metaclass for test cases with test generators. 349 350 A test generator is an iterable in a testcase that produces callables. These 351 callables must be single-argument methods. These methods are injected into 352 the class namespace and the original iterable is removed. If the name of the 353 iterable conforms to the test pattern, the injected methods will be picked 354 up as tests by the unittest framework. 355 356 In general, it is supposed to be used in conjunction with the 357 parameters decorator. 358 """ 359 360 def __new__(mcs, class_name, bases, dct): 361 dct['_id_suffix'] = id_suffix = {} 362 for name, obj in dct.items(): 363 if (name.startswith(unittest.TestLoader.testMethodPrefix) and 364 _NonStringIterable(obj)): 365 iterator = iter(obj) 366 dct.pop(name) 367 _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator) 368 369 return type.__new__(mcs, class_name, bases, dct) 370 371 372def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator): 373 """Adds individual test cases to a dictionary. 374 375 Args: 376 dct: The target dictionary. 377 id_suffix: The dictionary for mapping names to test IDs. 378 name: The original name of the test case. 379 iterator: The iterator generating the individual test cases. 380 """ 381 for idx, func in enumerate(iterator): 382 assert callable(func), 'Test generators must yield callables, got %r' % ( 383 func,) 384 if getattr(func, '__x_use_name__', False): 385 new_name = func.__name__ 386 else: 387 new_name = '%s%s%d' % (name, _SEPARATOR, idx) 388 assert new_name not in dct, ( 389 'Name of parameterized test case "%s" not unique' % (new_name,)) 390 dct[new_name] = func 391 id_suffix[new_name] = getattr(func, '__x_extra_id__', '') 392 393 394class TestCase(unittest.TestCase): 395 """Base class for test cases using the parameters decorator.""" 396 __metaclass__ = TestGeneratorMetaclass 397 398 def _OriginalName(self): 399 return self._testMethodName.split(_SEPARATOR)[0] 400 401 def __str__(self): 402 return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__)) 403 404 def id(self): # pylint: disable=invalid-name 405 """Returns the descriptive ID of the test. 406 407 This is used internally by the unittesting framework to get a name 408 for the test to be used in reports. 409 410 Returns: 411 The test id. 412 """ 413 return '%s.%s%s' % (_StrClass(self.__class__), 414 self._OriginalName(), 415 self._id_suffix.get(self._testMethodName, '')) 416 417 418def CoopTestCase(other_base_class): 419 """Returns a new base class with a cooperative metaclass base. 420 421 This enables the TestCase to be used in combination 422 with other base classes that have custom metaclasses, such as 423 mox.MoxTestBase. 424 425 Only works with metaclasses that do not override type.__new__. 426 427 Example: 428 429 import google3 430 import mox 431 432 from google3.testing.pybase import parameterized 433 434 class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)): 435 ... 436 437 Args: 438 other_base_class: (class) A test case base class. 439 440 Returns: 441 A new class object. 442 """ 443 metaclass = type( 444 'CoopMetaclass', 445 (other_base_class.__metaclass__, 446 TestGeneratorMetaclass), {}) 447 return metaclass( 448 'CoopTestCase', 449 (other_base_class, TestCase), {}) 450