• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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