blob: f2c0b305b9e63be01d775d19404dc1b40c903015 [file] [log] [blame]
Brian Silverman9c614bc2016-02-15 20:20:02 -05001#! /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
Austin Schuh40c16522018-10-28 20:27:54 -070040 class AdditionExample(parameterized.TestCase):
41 @parameterized.parameters(
Brian Silverman9c614bc2016-02-15 20:20:02 -050042 (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
Austin Schuh40c16522018-10-28 20:27:54 -070057 class AdditionExample(parameterized.TestCase):
58 @parameterized.parameters(
Brian Silverman9c614bc2016-02-15 20:20:02 -050059 {'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,
Austin Schuh40c16522018-10-28 20:27:54 -070080especially in test logs, you can use the named_parameters decorator. In
Brian Silverman9c614bc2016-02-15 20:20:02 -050081this 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
Austin Schuh40c16522018-10-28 20:27:54 -070085 class NamedExample(parameterized.TestCase):
86 @parameterized.named_parameters(
Brian Silverman9c614bc2016-02-15 20:20:02 -050087 ('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
Austin Schuh40c16522018-10-28 20:27:54 -0700106TestCase class, instead of decorating all test methods
Brian Silverman9c614bc2016-02-15 20:20:02 -0500107individually, the class itself can be decorated:
108
Austin Schuh40c16522018-10-28 20:27:54 -0700109 @parameterized.parameters(
Brian Silverman9c614bc2016-02-15 20:20:02 -0500110 (1, 2, 3)
111 (4, 5, 9))
Austin Schuh40c16522018-10-28 20:27:54 -0700112 class ArithmeticTest(parameterized.TestCase):
Brian Silverman9c614bc2016-02-15 20:20:02 -0500113 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
Austin Schuh40c16522018-10-28 20:27:54 -0700125 class AdditionExample(parameterized.TestCase):
126 @parameterized.parameters(
Brian Silverman9c614bc2016-02-15 20:20:02 -0500127 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
Austin Schuh40c16522018-10-28 20:27:54 -0700138 class NegativeNumberExample(parameterized.TestCase):
139 @parameterized.parameters(
Brian Silverman9c614bc2016-02-15 20:20:02 -0500140 -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.'
Austin Schuh40c16522018-10-28 20:27:54 -0700215 'TestCase. This is bad because none of '
Brian Silverman9c614bc2016-02-15 20:20:02 -0500216 '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
Austin Schuh40c16522018-10-28 20:27:54 -0700309def parameters(*testcases): # pylint: disable=invalid-name
Brian Silverman9c614bc2016-02-15 20:20:02 -0500310 """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
Austin Schuh40c16522018-10-28 20:27:54 -0700324def named_parameters(*testcases): # pylint: disable=invalid-name
Brian Silverman9c614bc2016-02-15 20:20:02 -0500325 """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
Austin Schuh40c16522018-10-28 20:27:54 -0700350 In general, it is supposed to be used in conjunction with the
351 parameters decorator.
Brian Silverman9c614bc2016-02-15 20:20:02 -0500352 """
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
Austin Schuh40c16522018-10-28 20:27:54 -0700388class TestCase(unittest.TestCase):
389 """Base class for test cases using the parameters decorator."""
Brian Silverman9c614bc2016-02-15 20:20:02 -0500390 __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
Austin Schuh40c16522018-10-28 20:27:54 -0700412def CoopTestCase(other_base_class):
Brian Silverman9c614bc2016-02-15 20:20:02 -0500413 """Returns a new base class with a cooperative metaclass base.
414
Austin Schuh40c16522018-10-28 20:27:54 -0700415 This enables the TestCase to be used in combination
Brian Silverman9c614bc2016-02-15 20:20:02 -0500416 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
Austin Schuh40c16522018-10-28 20:27:54 -0700428 class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)):
Brian Silverman9c614bc2016-02-15 20:20:02 -0500429 ...
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(
Austin Schuh40c16522018-10-28 20:27:54 -0700442 'CoopTestCase',
443 (other_base_class, TestCase), {})