421 lines
14 KiB
Python
421 lines
14 KiB
Python
#! /usr/bin/env python
|
|
#
|
|
# Protocol Buffers - Google's data interchange format
|
|
# Copyright 2008 Google Inc. All rights reserved.
|
|
#
|
|
# Use of this source code is governed by a BSD-style
|
|
# license that can be found in the LICENSE file or at
|
|
# https://developers.google.com/open-source/licenses/bsd
|
|
|
|
"""Adds support for parameterized tests to Python's unittest TestCase class.
|
|
|
|
A parameterized test is a method in a test case that is invoked with different
|
|
argument tuples.
|
|
|
|
A simple example:
|
|
|
|
class AdditionExample(_parameterized.TestCase):
|
|
@_parameterized.parameters(
|
|
(1, 2, 3),
|
|
(4, 5, 9),
|
|
(1, 1, 3))
|
|
def testAddition(self, op1, op2, result):
|
|
self.assertEqual(result, op1 + op2)
|
|
|
|
|
|
Each invocation is a separate test case and properly isolated just
|
|
like a normal test method, with its own setUp/tearDown cycle. In the
|
|
example above, there are three separate testcases, one of which will
|
|
fail due to an assertion error (1 + 1 != 3).
|
|
|
|
Parameters for individual test cases can be tuples (with positional parameters)
|
|
or dictionaries (with named parameters):
|
|
|
|
class AdditionExample(_parameterized.TestCase):
|
|
@_parameterized.parameters(
|
|
{'op1': 1, 'op2': 2, 'result': 3},
|
|
{'op1': 4, 'op2': 5, 'result': 9},
|
|
)
|
|
def testAddition(self, op1, op2, result):
|
|
self.assertEqual(result, op1 + op2)
|
|
|
|
If a parameterized test fails, the error message will show the
|
|
original test name (which is modified internally) and the arguments
|
|
for the specific invocation, which are part of the string returned by
|
|
the shortDescription() method on test cases.
|
|
|
|
The id method of the test, used internally by the unittest framework,
|
|
is also modified to show the arguments. To make sure that test names
|
|
stay the same across several invocations, object representations like
|
|
|
|
>>> class Foo(object):
|
|
... pass
|
|
>>> repr(Foo())
|
|
'<__main__.Foo object at 0x23d8610>'
|
|
|
|
are turned into '<__main__.Foo>'. For even more descriptive names,
|
|
especially in test logs, you can use the named_parameters decorator. In
|
|
this case, only tuples are supported, and the first parameters has to
|
|
be a string (or an object that returns an apt name when converted via
|
|
str()):
|
|
|
|
class NamedExample(_parameterized.TestCase):
|
|
@_parameterized.named_parameters(
|
|
('Normal', 'aa', 'aaa', True),
|
|
('EmptyPrefix', '', 'abc', True),
|
|
('BothEmpty', '', '', True))
|
|
def testStartsWith(self, prefix, string, result):
|
|
self.assertEqual(result, strings.startswith(prefix))
|
|
|
|
Named tests also have the benefit that they can be run individually
|
|
from the command line:
|
|
|
|
$ testmodule.py NamedExample.testStartsWithNormal
|
|
.
|
|
--------------------------------------------------------------------
|
|
Ran 1 test in 0.000s
|
|
|
|
OK
|
|
|
|
Parameterized Classes
|
|
=====================
|
|
If invocation arguments are shared across test methods in a single
|
|
TestCase class, instead of decorating all test methods
|
|
individually, the class itself can be decorated:
|
|
|
|
@_parameterized.parameters(
|
|
(1, 2, 3)
|
|
(4, 5, 9))
|
|
class ArithmeticTest(_parameterized.TestCase):
|
|
def testAdd(self, arg1, arg2, result):
|
|
self.assertEqual(arg1 + arg2, result)
|
|
|
|
def testSubtract(self, arg2, arg2, result):
|
|
self.assertEqual(result - arg1, arg2)
|
|
|
|
Inputs from Iterables
|
|
=====================
|
|
If parameters should be shared across several test cases, or are dynamically
|
|
created from other sources, a single non-tuple iterable can be passed into
|
|
the decorator. This iterable will be used to obtain the test cases:
|
|
|
|
class AdditionExample(_parameterized.TestCase):
|
|
@_parameterized.parameters(
|
|
c.op1, c.op2, c.result for c in testcases
|
|
)
|
|
def testAddition(self, op1, op2, result):
|
|
self.assertEqual(result, op1 + op2)
|
|
|
|
|
|
Single-Argument Test Methods
|
|
============================
|
|
If a test method takes only one argument, the single argument does not need to
|
|
be wrapped into a tuple:
|
|
|
|
class NegativeNumberExample(_parameterized.TestCase):
|
|
@_parameterized.parameters(
|
|
-1, -3, -4, -5
|
|
)
|
|
def testIsNegative(self, arg):
|
|
self.assertTrue(IsNegative(arg))
|
|
"""
|
|
|
|
__author__ = 'tmarek@google.com (Torsten Marek)'
|
|
|
|
import functools
|
|
import re
|
|
import types
|
|
import unittest
|
|
import uuid
|
|
|
|
try:
|
|
# Since python 3
|
|
import collections.abc as collections_abc
|
|
except ImportError:
|
|
# Won't work after python 3.8
|
|
import collections as collections_abc
|
|
|
|
ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>')
|
|
_SEPARATOR = uuid.uuid1().hex
|
|
_FIRST_ARG = object()
|
|
_ARGUMENT_REPR = object()
|
|
|
|
|
|
def _CleanRepr(obj):
|
|
return ADDR_RE.sub(r'<\1>', repr(obj))
|
|
|
|
|
|
# Helper function formerly from the unittest module, removed from it in
|
|
# Python 2.7.
|
|
def _StrClass(cls):
|
|
return '%s.%s' % (cls.__module__, cls.__name__)
|
|
|
|
|
|
def _NonStringIterable(obj):
|
|
return (isinstance(obj, collections_abc.Iterable) and
|
|
not isinstance(obj, str))
|
|
|
|
|
|
def _FormatParameterList(testcase_params):
|
|
if isinstance(testcase_params, collections_abc.Mapping):
|
|
return ', '.join('%s=%s' % (argname, _CleanRepr(value))
|
|
for argname, value in testcase_params.items())
|
|
elif _NonStringIterable(testcase_params):
|
|
return ', '.join(map(_CleanRepr, testcase_params))
|
|
else:
|
|
return _FormatParameterList((testcase_params,))
|
|
|
|
|
|
class _ParameterizedTestIter(object):
|
|
"""Callable and iterable class for producing new test cases."""
|
|
|
|
def __init__(self, test_method, testcases, naming_type):
|
|
"""Returns concrete test functions for a test and a list of parameters.
|
|
|
|
The naming_type is used to determine the name of the concrete
|
|
functions as reported by the unittest framework. If naming_type is
|
|
_FIRST_ARG, the testcases must be tuples, and the first element must
|
|
have a string representation that is a valid Python identifier.
|
|
|
|
Args:
|
|
test_method: The decorated test method.
|
|
testcases: (list of tuple/dict) A list of parameter
|
|
tuples/dicts for individual test invocations.
|
|
naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR.
|
|
"""
|
|
self._test_method = test_method
|
|
self.testcases = testcases
|
|
self._naming_type = naming_type
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
raise RuntimeError('You appear to be running a parameterized test case '
|
|
'without having inherited from parameterized.'
|
|
'TestCase. This is bad because none of '
|
|
'your test cases are actually being run.')
|
|
|
|
def __iter__(self):
|
|
test_method = self._test_method
|
|
naming_type = self._naming_type
|
|
|
|
def MakeBoundParamTest(testcase_params):
|
|
@functools.wraps(test_method)
|
|
def BoundParamTest(self):
|
|
if isinstance(testcase_params, collections_abc.Mapping):
|
|
test_method(self, **testcase_params)
|
|
elif _NonStringIterable(testcase_params):
|
|
test_method(self, *testcase_params)
|
|
else:
|
|
test_method(self, testcase_params)
|
|
|
|
if naming_type is _FIRST_ARG:
|
|
# Signal the metaclass that the name of the test function is unique
|
|
# and descriptive.
|
|
BoundParamTest.__x_use_name__ = True
|
|
BoundParamTest.__name__ += str(testcase_params[0])
|
|
testcase_params = testcase_params[1:]
|
|
elif naming_type is _ARGUMENT_REPR:
|
|
# __x_extra_id__ is used to pass naming information to the __new__
|
|
# method of TestGeneratorMetaclass.
|
|
# The metaclass will make sure to create a unique, but nondescriptive
|
|
# name for this test.
|
|
BoundParamTest.__x_extra_id__ = '(%s)' % (
|
|
_FormatParameterList(testcase_params),)
|
|
else:
|
|
raise RuntimeError('%s is not a valid naming type.' % (naming_type,))
|
|
|
|
BoundParamTest.__doc__ = '%s(%s)' % (
|
|
BoundParamTest.__name__, _FormatParameterList(testcase_params))
|
|
if test_method.__doc__:
|
|
BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,)
|
|
return BoundParamTest
|
|
return (MakeBoundParamTest(c) for c in self.testcases)
|
|
|
|
|
|
def _IsSingletonList(testcases):
|
|
"""True iff testcases contains only a single non-tuple element."""
|
|
return len(testcases) == 1 and not isinstance(testcases[0], tuple)
|
|
|
|
|
|
def _ModifyClass(class_object, testcases, naming_type):
|
|
assert not getattr(class_object, '_id_suffix', None), (
|
|
'Cannot add parameters to %s,'
|
|
' which already has parameterized methods.' % (class_object,))
|
|
class_object._id_suffix = id_suffix = {}
|
|
# We change the size of __dict__ while we iterate over it,
|
|
# which Python 3.x will complain about, so use copy().
|
|
for name, obj in class_object.__dict__.copy().items():
|
|
if (name.startswith(unittest.TestLoader.testMethodPrefix)
|
|
and isinstance(obj, types.FunctionType)):
|
|
delattr(class_object, name)
|
|
methods = {}
|
|
_UpdateClassDictForParamTestCase(
|
|
methods, id_suffix, name,
|
|
_ParameterizedTestIter(obj, testcases, naming_type))
|
|
for name, meth in methods.items():
|
|
setattr(class_object, name, meth)
|
|
|
|
|
|
def _ParameterDecorator(naming_type, testcases):
|
|
"""Implementation of the parameterization decorators.
|
|
|
|
Args:
|
|
naming_type: The naming type.
|
|
testcases: Testcase parameters.
|
|
|
|
Returns:
|
|
A function for modifying the decorated object.
|
|
"""
|
|
def _Apply(obj):
|
|
if isinstance(obj, type):
|
|
_ModifyClass(
|
|
obj,
|
|
list(testcases) if not isinstance(testcases, collections_abc.Sequence)
|
|
else testcases,
|
|
naming_type)
|
|
return obj
|
|
else:
|
|
return _ParameterizedTestIter(obj, testcases, naming_type)
|
|
|
|
if _IsSingletonList(testcases):
|
|
assert _NonStringIterable(testcases[0]), (
|
|
'Single parameter argument must be a non-string iterable')
|
|
testcases = testcases[0]
|
|
|
|
return _Apply
|
|
|
|
|
|
def parameters(*testcases): # pylint: disable=invalid-name
|
|
"""A decorator for creating parameterized tests.
|
|
|
|
See the module docstring for a usage example.
|
|
Args:
|
|
*testcases: Parameters for the decorated method, either a single
|
|
iterable, or a list of tuples/dicts/objects (for tests
|
|
with only one argument).
|
|
|
|
Returns:
|
|
A test generator to be handled by TestGeneratorMetaclass.
|
|
"""
|
|
return _ParameterDecorator(_ARGUMENT_REPR, testcases)
|
|
|
|
|
|
def named_parameters(*testcases): # pylint: disable=invalid-name
|
|
"""A decorator for creating parameterized tests.
|
|
|
|
See the module docstring for a usage example. The first element of
|
|
each parameter tuple should be a string and will be appended to the
|
|
name of the test method.
|
|
|
|
Args:
|
|
*testcases: Parameters for the decorated method, either a single
|
|
iterable, or a list of tuples.
|
|
|
|
Returns:
|
|
A test generator to be handled by TestGeneratorMetaclass.
|
|
"""
|
|
return _ParameterDecorator(_FIRST_ARG, testcases)
|
|
|
|
|
|
class TestGeneratorMetaclass(type):
|
|
"""Metaclass for test cases with test generators.
|
|
|
|
A test generator is an iterable in a testcase that produces callables. These
|
|
callables must be single-argument methods. These methods are injected into
|
|
the class namespace and the original iterable is removed. If the name of the
|
|
iterable conforms to the test pattern, the injected methods will be picked
|
|
up as tests by the unittest framework.
|
|
|
|
In general, it is supposed to be used in conjunction with the
|
|
parameters decorator.
|
|
"""
|
|
|
|
def __new__(mcs, class_name, bases, dct):
|
|
dct['_id_suffix'] = id_suffix = {}
|
|
for name, obj in dct.copy().items():
|
|
if (name.startswith(unittest.TestLoader.testMethodPrefix) and
|
|
_NonStringIterable(obj)):
|
|
iterator = iter(obj)
|
|
dct.pop(name)
|
|
_UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator)
|
|
|
|
return type.__new__(mcs, class_name, bases, dct)
|
|
|
|
|
|
def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator):
|
|
"""Adds individual test cases to a dictionary.
|
|
|
|
Args:
|
|
dct: The target dictionary.
|
|
id_suffix: The dictionary for mapping names to test IDs.
|
|
name: The original name of the test case.
|
|
iterator: The iterator generating the individual test cases.
|
|
"""
|
|
for idx, func in enumerate(iterator):
|
|
assert callable(func), 'Test generators must yield callables, got %r' % (
|
|
func,)
|
|
if getattr(func, '__x_use_name__', False):
|
|
new_name = func.__name__
|
|
else:
|
|
new_name = '%s%s%d' % (name, _SEPARATOR, idx)
|
|
assert new_name not in dct, (
|
|
'Name of parameterized test case "%s" not unique' % (new_name,))
|
|
dct[new_name] = func
|
|
id_suffix[new_name] = getattr(func, '__x_extra_id__', '')
|
|
|
|
|
|
class TestCase(unittest.TestCase, metaclass=TestGeneratorMetaclass):
|
|
"""Base class for test cases using the parameters decorator."""
|
|
|
|
def _OriginalName(self):
|
|
return self._testMethodName.split(_SEPARATOR)[0]
|
|
|
|
def __str__(self):
|
|
return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__))
|
|
|
|
def id(self): # pylint: disable=invalid-name
|
|
"""Returns the descriptive ID of the test.
|
|
|
|
This is used internally by the unittesting framework to get a name
|
|
for the test to be used in reports.
|
|
|
|
Returns:
|
|
The test id.
|
|
"""
|
|
return '%s.%s%s' % (_StrClass(self.__class__),
|
|
self._OriginalName(),
|
|
self._id_suffix.get(self._testMethodName, ''))
|
|
|
|
|
|
def CoopTestCase(other_base_class):
|
|
"""Returns a new base class with a cooperative metaclass base.
|
|
|
|
This enables the TestCase to be used in combination
|
|
with other base classes that have custom metaclasses, such as
|
|
mox.MoxTestBase.
|
|
|
|
Only works with metaclasses that do not override type.__new__.
|
|
|
|
Example:
|
|
|
|
import google3
|
|
import mox
|
|
|
|
from google.protobuf.internal import _parameterized
|
|
|
|
class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)):
|
|
...
|
|
|
|
Args:
|
|
other_base_class: (class) A test case base class.
|
|
|
|
Returns:
|
|
A new class object.
|
|
"""
|
|
metaclass = type(
|
|
'CoopMetaclass',
|
|
(other_base_class.__metaclass__,
|
|
TestGeneratorMetaclass), {})
|
|
return metaclass(
|
|
'CoopTestCase',
|
|
(other_base_class, TestCase), {})
|