Source code for plainbox.testing_utils.testcases

# This file is part of Checkbox.
#
# Copyright 2012 Canonical Ltd.
# Written by:
#   Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
#
# Checkbox is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3,
# as published by the Free Software Foundation.

#
# Checkbox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.

"""
:mod:`plainbox.testing_utils.testcases` -- additional TestCase classes
======================================================================

Implementation of additional TestCase classes that aid in testing.
"""

from unittest import TestCase
from unittest.util import strclass


[docs]def load_tests(loader, suite, pattern): # Because of the default patterns used to discover test cases # we need to prevent the unittest machinery from treating the # TestCase* classes as actual tests. # # The discovery mechanism has little-known feature whereas the discovery # can be customized on a per-package and per-module level by defining the # load_tests() function. This function must return a TestSuite object # # Here we simply return an empty suite. return loader.suiteClass([])
# Treat this as a part of TestCase implementation detail, removing it from the # tracebacks that are reported to the developer. __unittest = True
[docs]class TestCaseParameters: """ Class for holding test case parameters Instances of this class are provided as the .parameters attribute to instances of TestCaseWithParameters during run(). The class supports attribute lookup so tests can be written in convenient form. Having a test case with parameters 'foo' and 'bar' they can be accessed as self.parameters.foo and self.parameters.bar respectively. """ __slots__ = ('_names', '_values') def __init__(self, names, values): """ Initialize with a tuple of parameter names and values. Both arguments have to be tuples. """ self._names = names self._values = values def __eq__(self, other): if not isinstance(other, TestCaseParameters): return NotImplemented return (self._names == other._names and self._values == other._values) def __getattr__(self, attr): try: return self._values[self._names.index(attr)] except (ValueError, LookupError): # index() raises ValueError # [] raises LookupError subclass raise AttributeError(attr) def __str__(self): return ", ".join([ "{}: {}".format(name, value) for name, value in zip(self._names, self._values)]) def __repr__(self): return "<{} {}>".format(self.__class__.__name__, str(self))
[docs]class TestCaseWithParameters(TestCase): """ TestCase parametrized by any number of named parameters. This class can save a lot of typing or creating dummy base classes and other implementation tricks that aim to reduce duplication in code that tests multiple values of a parameter. For all intents and purposes this is just a standard TestClass instance, what makes it different are the few tricks employed to make it appear as a collection of test cases instead. Each TestCase created from the test_** methods will be invoked for all the parameter values. Any number of parameters that can be provided. A tuple of values must be provided for each parameter. In simple cases the developer only needs to provide two class attributes, parameter_names and parameter_values. In more complex cases behavior can be customized by defining two class methods, get_parameter_names() and get_parameter_values(). Note that you _must_ define a non-empty list of parameter values, otherwise this test will behave as if it never existed (analogous how multiplication by zero works). .. note:: Technical note for tinkerers and subclass authors. Python unittest framework is pretty annoying to work with or extend. In practice you should always keep the source code (of a particular python version) open to reason about it. Parametrization is implemented by creating additional instances of TestCaseWithParameters with immutable, bound, parameters. That logic is implemented in run(). The multiplication of TestCase instances happens in _parametrize(). If your sub-class needs to do something special there you might need to override it. Ideally the unittest framework could allow customizing discovery in a standard way. If that were true then we could instantiate parametrized copies early and then let the normal run() mechanics work. Sadly this is not the case. Most special python methods also had to be overridden to take account of the new _parameters instance attribute. This includes __str__(), __repr__(), __eq__() and __hash__(). Additional methods unique to unittest framework were implemented to convey the value of the parameter back to the user / developer. Those include id() and countTestCases() """ def __init__(self, methodName="runTest", parameters=None): """ Overridden method of TestCase Instantiates a new TestCase that will run a given method. By default creates the 'unparameterized' version which spawns real test cases (with the bound parameter) when .run() is called. """ super(TestCaseWithParameters, self).__init__(methodName) self._parameters = parameters @property def parameters(self): """ Return the value of parameters that specialize this test case Normal instances always return None here, the special, parametrized, instance that is constructed once for each parameter value during run() (where testing actually happens), returns the real value. """ return self._parameters parameter_names = ()
[docs] @classmethod def get_parameter_names(cls): """ Return a tuple of parameters that affect this test case. """ return cls.parameter_names
parameter_values = ()
[docs] @classmethod def get_parameter_values(cls): """ Return a tuple of tuple of values that should be mapped to subsequent attributes named, as returned by get_parameter_names() """ return cls.parameter_values
def _parametrize(self, parameters): """ Internal implementation method of TestCaseWithParameters. Creates a new instance of the sub-class that is about to be tested binding the particular value of the parameters supplied. This is where test cases are actually instantiated / multiplied. """ return type(self)(self._testMethodName, parameters)
[docs] def countTestCases(self): """ Overridden method of unittest.TestCase() Behaves different depending on whether it is called on the original instance or the parametrized instance. The original instance behaves like a TestSuite, returning the number of parameter values. Each parametrized instance (created with _parametrize()) returns 1 """ if self.parameters is None: try: return len(self.get_parameter_values()) except TypeError: # if get_parameter_values() returns a generator then # we need to count it ourselves return len(list(self.get_parameter_values())) else: return 1
[docs] def run(self, result=None): """ Overridden version of TestCase.run() Creates additional instances of the class being tested, one for each value returned by get_parameter_values() by calling _parametrize(). Each of the parametrized instances is tested normally. """ # Get a result object if we don't have one if result is None: result = self.defaultTestResult() # Get the list of parameter names names = self.get_parameter_names() # For each list of parameter values: for iteration_index, values in enumerate(self.get_parameter_values()): if len(values) != len(names): raise RuntimeError(( "incorrect get_parameter_values() or parameter_values for" " iteration {}. Expected to see {} item but saw {} instead" ).format(iteration_index, len(names), len(values))) # Construct the parameter placeholder parameters = TestCaseParameters(names, values) # Construct a parametrized version of this test case parametrized_test_case = self._parametrize(parameters) # Super-call the run() method of the new instance super(TestCaseWithParameters, parametrized_test_case).run(result)
[docs] def id(self): """ Overridden version of TestCase.id() This is an internal implementation detail of TestCase, it is used in certain places instead of __str__(). It behaves very similar to what __str__() does, except that it displays the class name differently """ if self.parameters is None: return "{}.{} [<unparameterized>]".format( strclass(self.__class__), self._testMethodName) else: return "{}.{} [{}]".format( strclass(self.__class__), self._testMethodName, self.parameters)
def __str__(self): """ Overridden version of TestCase.__str__() This version takes the current value of parameters into account. During testing in run(), when parameters are not None, they are added to the value returned by the superclass. At other times the string <unparameterized> is used. """ if self.parameters is None: return "{} [<unparameterized>]".format( super(TestCaseWithParameters, self).__str__()) else: return "{} [{}]".format( super(TestCaseWithParameters, self).__str__(), self.parameters) def __repr__(self): """ Overridden version of TestCase.__repr__() This version displays the value of the parameters attribute """ return "<{} testMethod={} parameters={!r}>".format( strclass(self.__class__), self._testMethodName, self.parameters) def __eq__(self, other): """ Overridden version of TestCase.__eq__() This version also compares the parameters attribute """ if not isinstance(other, TestCaseWithParameters): return NotImplemented return (self._testMethodName == other._testMethodName and self._parameters == other.parameters) def __hash__(self): """ Overridden version of TestCase.__hash__() This version also uses the parameters attribute """ return hash((type(self), self._testMethodName, self._parameters))
comments powered by Disqus