Brian Silverman | 9c614bc | 2016-02-15 20:20:02 -0500 | [diff] [blame^] | 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 | |
| 35 | A parameterized test is a method in a test case that is invoked with different |
| 36 | argument tuples. |
| 37 | |
| 38 | A 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 | |
| 49 | Each invocation is a separate test case and properly isolated just |
| 50 | like a normal test method, with its own setUp/tearDown cycle. In the |
| 51 | example above, there are three separate testcases, one of which will |
| 52 | fail due to an assertion error (1 + 1 != 3). |
| 53 | |
| 54 | Parameters for invididual test cases can be tuples (with positional parameters) |
| 55 | or 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 | |
| 65 | If a parameterized test fails, the error message will show the |
| 66 | original test name (which is modified internally) and the arguments |
| 67 | for the specific invocation, which are part of the string returned by |
| 68 | the shortDescription() method on test cases. |
| 69 | |
| 70 | The id method of the test, used internally by the unittest framework, |
| 71 | is also modified to show the arguments. To make sure that test names |
| 72 | stay 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 | |
| 79 | are turned into '<__main__.Foo>'. For even more descriptive names, |
| 80 | especially in test logs, you can use the NamedParameters decorator. In |
| 81 | this case, only tuples are supported, and the first parameters has to |
| 82 | be a string (or an object that returns an apt name when converted via |
| 83 | str()): |
| 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 | |
| 93 | Named tests also have the benefit that they can be run individually |
| 94 | from the command line: |
| 95 | |
| 96 | $ testmodule.py NamedExample.testStartsWithNormal |
| 97 | . |
| 98 | -------------------------------------------------------------------- |
| 99 | Ran 1 test in 0.000s |
| 100 | |
| 101 | OK |
| 102 | |
| 103 | Parameterized Classes |
| 104 | ===================== |
| 105 | If invocation arguments are shared across test methods in a single |
| 106 | ParameterizedTestCase class, instead of decorating all test methods |
| 107 | individually, 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 | |
| 119 | Inputs from Iterables |
| 120 | ===================== |
| 121 | If parameters should be shared across several test cases, or are dynamically |
| 122 | created from other sources, a single non-tuple iterable can be passed into |
| 123 | the 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 | |
| 133 | Single-Argument Test Methods |
| 134 | ============================ |
| 135 | If a test method takes only one argument, the single argument does not need to |
| 136 | be 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 | |
| 148 | import collections |
| 149 | import functools |
| 150 | import re |
| 151 | import types |
| 152 | try: |
| 153 | import unittest2 as unittest |
| 154 | except ImportError: |
| 155 | import unittest |
| 156 | import uuid |
| 157 | |
| 158 | import six |
| 159 | |
| 160 | ADDR_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 | |
| 166 | def _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. |
| 172 | def _StrClass(cls): |
| 173 | return '%s.%s' % (cls.__module__, cls.__name__) |
| 174 | |
| 175 | |
| 176 | def _NonStringIterable(obj): |
| 177 | return (isinstance(obj, collections.Iterable) and not |
| 178 | isinstance(obj, six.string_types)) |
| 179 | |
| 180 | |
| 181 | def _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 | |
| 191 | class _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 | |
| 256 | def _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 | |
| 261 | def _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 | |
| 280 | def _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 | |
| 309 | def 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 | |
| 324 | def 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 | |
| 341 | class 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 | |
| 366 | def _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 | |
| 388 | class 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 | |
| 412 | def 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), {}) |