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