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.ParameterizedTestCase): 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.ParameterizedTestCase): 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 NamedParameters 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.ParameterizedTestCase): 86 @parameterized.NamedParameters( 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 106ParameterizedTestCase 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.ParameterizedTestCase): 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.ParameterizedTestCase): 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.ParameterizedTestCase): 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 collections 149import functools 150import re 151import types 152try: 153 import unittest2 as unittest 154except ImportError: 155 import unittest 156import uuid 157 158import six 159 160ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') 161_SEPARATOR = uuid.uuid1().hex 162_FIRST_ARG = object() 163_ARGUMENT_REPR = object() 164 165 166def _CleanRepr(obj): 167 return ADDR_RE.sub(r'<\1>', repr(obj)) 168 169 170# Helper function formerly from the unittest module, removed from it in 171# Python 2.7. 172def _StrClass(cls): 173 return '%s.%s' % (cls.__module__, cls.__name__) 174 175 176def _NonStringIterable(obj): 177 return (isinstance(obj, collections.Iterable) and not 178 isinstance(obj, six.string_types)) 179 180 181def _FormatParameterList(testcase_params): 182 if isinstance(testcase_params, collections.Mapping): 183 return ', '.join('%s=%s' % (argname, _CleanRepr(value)) 184 for argname, value in testcase_params.items()) 185 elif _NonStringIterable(testcase_params): 186 return ', '.join(map(_CleanRepr, testcase_params)) 187 else: 188 return _FormatParameterList((testcase_params,)) 189 190 191class _ParameterizedTestIter(object): 192 """Callable and iterable class for producing new test cases.""" 193 194 def __init__(self, test_method, testcases, naming_type): 195 """Returns concrete test functions for a test and a list of parameters. 196 197 The naming_type is used to determine the name of the concrete 198 functions as reported by the unittest framework. If naming_type is 199 _FIRST_ARG, the testcases must be tuples, and the first element must 200 have a string representation that is a valid Python identifier. 201 202 Args: 203 test_method: The decorated test method. 204 testcases: (list of tuple/dict) A list of parameter 205 tuples/dicts for individual test invocations. 206 naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. 207 """ 208 self._test_method = test_method 209 self.testcases = testcases 210 self._naming_type = naming_type 211 212 def __call__(self, *args, **kwargs): 213 raise RuntimeError('You appear to be running a parameterized test case ' 214 'without having inherited from parameterized.' 215 'ParameterizedTestCase. This is bad because none of ' 216 'your test cases are actually being run.') 217 218 def __iter__(self): 219 test_method = self._test_method 220 naming_type = self._naming_type 221 222 def MakeBoundParamTest(testcase_params): 223 @functools.wraps(test_method) 224 def BoundParamTest(self): 225 if isinstance(testcase_params, collections.Mapping): 226 test_method(self, **testcase_params) 227 elif _NonStringIterable(testcase_params): 228 test_method(self, *testcase_params) 229 else: 230 test_method(self, testcase_params) 231 232 if naming_type is _FIRST_ARG: 233 # Signal the metaclass that the name of the test function is unique 234 # and descriptive. 235 BoundParamTest.__x_use_name__ = True 236 BoundParamTest.__name__ += str(testcase_params[0]) 237 testcase_params = testcase_params[1:] 238 elif naming_type is _ARGUMENT_REPR: 239 # __x_extra_id__ is used to pass naming information to the __new__ 240 # method of TestGeneratorMetaclass. 241 # The metaclass will make sure to create a unique, but nondescriptive 242 # name for this test. 243 BoundParamTest.__x_extra_id__ = '(%s)' % ( 244 _FormatParameterList(testcase_params),) 245 else: 246 raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) 247 248 BoundParamTest.__doc__ = '%s(%s)' % ( 249 BoundParamTest.__name__, _FormatParameterList(testcase_params)) 250 if test_method.__doc__: 251 BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,) 252 return BoundParamTest 253 return (MakeBoundParamTest(c) for c in self.testcases) 254 255 256def _IsSingletonList(testcases): 257 """True iff testcases contains only a single non-tuple element.""" 258 return len(testcases) == 1 and not isinstance(testcases[0], tuple) 259 260 261def _ModifyClass(class_object, testcases, naming_type): 262 assert not getattr(class_object, '_id_suffix', None), ( 263 'Cannot add parameters to %s,' 264 ' which already has parameterized methods.' % (class_object,)) 265 class_object._id_suffix = id_suffix = {} 266 # We change the size of __dict__ while we iterate over it, 267 # which Python 3.x will complain about, so use copy(). 268 for name, obj in class_object.__dict__.copy().items(): 269 if (name.startswith(unittest.TestLoader.testMethodPrefix) 270 and isinstance(obj, types.FunctionType)): 271 delattr(class_object, name) 272 methods = {} 273 _UpdateClassDictForParamTestCase( 274 methods, id_suffix, name, 275 _ParameterizedTestIter(obj, testcases, naming_type)) 276 for name, meth in methods.items(): 277 setattr(class_object, name, meth) 278 279 280def _ParameterDecorator(naming_type, testcases): 281 """Implementation of the parameterization decorators. 282 283 Args: 284 naming_type: The naming type. 285 testcases: Testcase parameters. 286 287 Returns: 288 A function for modifying the decorated object. 289 """ 290 def _Apply(obj): 291 if isinstance(obj, type): 292 _ModifyClass( 293 obj, 294 list(testcases) if not isinstance(testcases, collections.Sequence) 295 else testcases, 296 naming_type) 297 return obj 298 else: 299 return _ParameterizedTestIter(obj, testcases, naming_type) 300 301 if _IsSingletonList(testcases): 302 assert _NonStringIterable(testcases[0]), ( 303 'Single parameter argument must be a non-string iterable') 304 testcases = testcases[0] 305 306 return _Apply 307 308 309def Parameters(*testcases): 310 """A decorator for creating parameterized tests. 311 312 See the module docstring for a usage example. 313 Args: 314 *testcases: Parameters for the decorated method, either a single 315 iterable, or a list of tuples/dicts/objects (for tests 316 with only one argument). 317 318 Returns: 319 A test generator to be handled by TestGeneratorMetaclass. 320 """ 321 return _ParameterDecorator(_ARGUMENT_REPR, testcases) 322 323 324def NamedParameters(*testcases): 325 """A decorator for creating parameterized tests. 326 327 See the module docstring for a usage example. The first element of 328 each parameter tuple should be a string and will be appended to the 329 name of the test method. 330 331 Args: 332 *testcases: Parameters for the decorated method, either a single 333 iterable, or a list of tuples. 334 335 Returns: 336 A test generator to be handled by TestGeneratorMetaclass. 337 """ 338 return _ParameterDecorator(_FIRST_ARG, testcases) 339 340 341class TestGeneratorMetaclass(type): 342 """Metaclass for test cases with test generators. 343 344 A test generator is an iterable in a testcase that produces callables. These 345 callables must be single-argument methods. These methods are injected into 346 the class namespace and the original iterable is removed. If the name of the 347 iterable conforms to the test pattern, the injected methods will be picked 348 up as tests by the unittest framework. 349 350 In general, it is supposed to be used in conjuction with the 351 Parameters decorator. 352 """ 353 354 def __new__(mcs, class_name, bases, dct): 355 dct['_id_suffix'] = id_suffix = {} 356 for name, obj in dct.items(): 357 if (name.startswith(unittest.TestLoader.testMethodPrefix) and 358 _NonStringIterable(obj)): 359 iterator = iter(obj) 360 dct.pop(name) 361 _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator) 362 363 return type.__new__(mcs, class_name, bases, dct) 364 365 366def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator): 367 """Adds individual test cases to a dictionary. 368 369 Args: 370 dct: The target dictionary. 371 id_suffix: The dictionary for mapping names to test IDs. 372 name: The original name of the test case. 373 iterator: The iterator generating the individual test cases. 374 """ 375 for idx, func in enumerate(iterator): 376 assert callable(func), 'Test generators must yield callables, got %r' % ( 377 func,) 378 if getattr(func, '__x_use_name__', False): 379 new_name = func.__name__ 380 else: 381 new_name = '%s%s%d' % (name, _SEPARATOR, idx) 382 assert new_name not in dct, ( 383 'Name of parameterized test case "%s" not unique' % (new_name,)) 384 dct[new_name] = func 385 id_suffix[new_name] = getattr(func, '__x_extra_id__', '') 386 387 388class ParameterizedTestCase(unittest.TestCase): 389 """Base class for test cases using the Parameters decorator.""" 390 __metaclass__ = TestGeneratorMetaclass 391 392 def _OriginalName(self): 393 return self._testMethodName.split(_SEPARATOR)[0] 394 395 def __str__(self): 396 return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__)) 397 398 def id(self): # pylint: disable=invalid-name 399 """Returns the descriptive ID of the test. 400 401 This is used internally by the unittesting framework to get a name 402 for the test to be used in reports. 403 404 Returns: 405 The test id. 406 """ 407 return '%s.%s%s' % (_StrClass(self.__class__), 408 self._OriginalName(), 409 self._id_suffix.get(self._testMethodName, '')) 410 411 412def CoopParameterizedTestCase(other_base_class): 413 """Returns a new base class with a cooperative metaclass base. 414 415 This enables the ParameterizedTestCase to be used in combination 416 with other base classes that have custom metaclasses, such as 417 mox.MoxTestBase. 418 419 Only works with metaclasses that do not override type.__new__. 420 421 Example: 422 423 import google3 424 import mox 425 426 from google3.testing.pybase import parameterized 427 428 class ExampleTest(parameterized.CoopParameterizedTestCase(mox.MoxTestBase)): 429 ... 430 431 Args: 432 other_base_class: (class) A test case base class. 433 434 Returns: 435 A new class object. 436 """ 437 metaclass = type( 438 'CoopMetaclass', 439 (other_base_class.__metaclass__, 440 TestGeneratorMetaclass), {}) 441 return metaclass( 442 'CoopParameterizedTestCase', 443 (other_base_class, ParameterizedTestCase), {}) 444