[Python-Dev] PEP: Frequently-requested additional features for the `unittest` module

Ben Finney ben+python at benfinney.id.au
Wed Jul 16 07:48:09 CEST 2008


Significant changes: Add a new 'TestLoader.load_tests_from_collection'
method, with full reference implementation. This makes the 'run_tests'
reference implementation straightforward.


:PEP:           XXX
:Title:         Frequently-requested additional features for the `unittest` module
:Version:       0.4
:Last-Modified: 2008-07-16
:Author:        Ben Finney <ben+python at benfinney.id.au>
:Status:        Draft
:Type:          Standards Track
:Content-Type:  test/x-rst
:Requires:      PEP XXX (Consolidating names in the `unittest` module)
:Created:       2008-07-14
:Post-History:


..  contents::


Abstract
========

This PEP proposes frequently-requested additions to the standard
library `unittest` module that are natural extensions of its existing
functionality.


Motivation
==========

The `unittest` module is functionally complete. Nevertheless, many
users request and devise extensions to perform functions that are so
common they deserve to have a standard implementation.


Specification
=============

New condition tests
-------------------

The following test methods will be added to the ``TestCase`` class.
The function signature is part of this specification. The body is to
be treated as a reference implementation only; any functionally
identical implementation also meets this specification.

::

    import operator
    import re

    class TestCase(object):
        # …

        def assert_compare_true(op, first, second, msg=None):
            if msg is None:
                msg = "%(first)r %(op)r %(second)" % vars()
            if not op(first, second):
                raise self.failure_exception(msg)

        def assert_in(container, member, msg=None):
            op = operator.__contains__
            self.assert_compare_true(op, container, member, msg)

        def assert_is(first, second, msg=None):
            op = operator.is_
            self.assert_compare_true(op, first, second, msg)

        def assert_less_than(first, second, msg=None):
            op = operator.lt
            self.assert_compare_true(op, first, second, msg)

        def assert_greater_than(first, second, msg=None):
            op = operator.gt
            self.assert_compare_true(op, first, second, msg)

        def assert_less_than_or_equal(first, second, msg=None):
            op = operator.le
            self.assert_compare_true(op, first, second, msg)

        def assert_greater_than_or_equal(first, second, msg=None):
            op = operator.ge
            self.assert_compare_true(op, first, second, msg)

        def assert_members_equal(first, second, msg=None):
            self.assert_equal(set(first), set(second), msg)

        def assert_sequence_equal(first, second, msg=None):
            self.assert_equal(list(first), list(second), msg)

        def assert_raises_with_message_regex(
            exc_class, message_regex, callable_obj, *args, **kwargs):
            exc_name = exc_class.__name__
            message_pattern = re.compile(message_regex)
            try:
                callable_obj(*args, **kwargs)
            except exc_class, exc:
                exc_message = str(exc)
                if not message_pattern.match(exc_message):
                    msg = (
                        "%(exc_name)s raised"
                        " without message matching %(message_regex)r"
                        " (got message %(exc_message)r)"
                        ) % vars()
                    raise self.failure_exception(msg)
            else:
                msg = "%(exc_name)s not raised" % vars()
                raise self.failure_exception(msg)

The following test methods are also added. Their implementation in
each case is simply the logical inverse of a corresponding method
above.

::

        def assert_compare_false(op, first, second, msg=None):
            # Logical inverse of assert_compare_true

        def assert_not_in(container, member, msg=None):
            # Logical inverse of assert_in

        def assert_is_not(first, second, msg=None)
            # Logical inverse of assert_is

        def assert_not_less_than(first, second, msg=None)
            # Logical inverse of assert_less_than

        def assert_not_greater_than(first, second, msg=None)
            # Logical inverse of assert_greater_than

        def assert_not_less_than_or_equal(first, second, msg=None)
            # Logical inverse of assert_less_than_or_equal

        def assert_not_greater_than_or_equal(first, second, msg=None)
            # Logical inverse of assert_greater_than_or_equal

        def assert_members_not_equal(first, second, msg=None)
            # Logical inverse of assert_members_equal

        def assert_sequence_not_equal(first, second, msg=None)
            # Logical inverse of assert_sequence_equal

Enhanced failure message for equality tests
-------------------------------------------

The equality tests will change their behaviour such that the message
always, even if overridden with a specific message when called,
includes extra information:

* For both ``assert_equal`` and ``assert_not_equal``: the ``repr()``
  output of the objects that were compared.

* For ``assert_equal`` comparisons of ``basestring`` instances that
  are multi-line text: the output of ``diff`` comparing the two texts.

* For membership comparisons with ``assert_*_equal``: the ``repr()``
  output of the members that were not equal in each collection. (This
  change is not done for the corresponding ``assert_*_not_equal``
  tests, which only fail if the collection members are equal.)

Simple invocation of test collection
------------------------------------

To allow simpler loading and running of test cases from an arbitrary
collection, the following new functionality will be added to the
`unittest` module. The function signatures are part of this
specification; the implementation is to be considered a reference
implementation only.

::

    class TestLoader(object):
        # …

        def load_tests_from_collection(self, collection):
            """ Return a suite of all test cases found in `collection`

                :param collection:
                    One of the following:
                    * a `TestSuite` instance
                    * a `TestCase` subclass
                    * a module
                    * an iterable producing any of these types
                :return:
                    A `TestSuite` instance containing all the test
                    cases found recursively within `collection`.

                """
            suite = None
            if isinstance(collection, TestSuite):
                suite = collection
            elif isinstance(collection, type):
                if issubclass(collection, TestCase):
                    suite = self.load_tests_from_test_case(collection)
            elif isinstance(collection, types.ModuleType):
                suite = self.load_tests_from_module(collection)
            elif hasattr(collection, '__iter__'):
                suite = self.suite_class()
                for item in collection:
                    suite.add_test(self.load_tests_from_collection(item))
            else:
                msg = "not a test collection: %(collection)r" % vars()
                raise TypeError(msg)
            return suite


    def run_tests(
        tests,
        loader_class=TestLoader, runner_class=TextTestRunner):
        """ Run a collection of tests with a test runner

            :param tests:
                A test collection as defined by the `loader_class`
                method `load_tests_from_collection`.
            :param loader_class:
                The type of test loader to use for collecting tests
                from the `tests` collection.
            :param runner_class:
                The type of test runner to instantiate for running the
                collected test suite.
            :return:
                None.

            """
        loader = loader_class()
        suite = loader.load_tests_from_collection(tests)
        runner = runner_class()
        runner.run(suite)


Rationale
=========

Names for logical-inverse tests
-------------------------------

The simple pattern established by ``assert_foo`` having a logical
inverse named ``assert_not_foo`` sometimes results in gramatically
awkward names. The following names were chosen in exception of this
pattern, in the interest of the API names being more intuitive:

* ``assert_is_not``
* ``assert_members_not_equal``
* ``assert_sequence_not_equal``

Order of method parameters
--------------------------

The methods ``assert_in``, ``assert_not_in`` have the container as the
first parameter. This makes the grammatical object of the function
name come immediately after the function name: "Assert in
`container`". This matches the convention set by the existing
``assert_raises(exception, callable_obj, …)`` "(Assert the code
raises `exception`").


Backwards Compatibility
=======================

This PEP proposes only additional features. There are no
backward-incompatible changes.


Reference Implementation
========================

None yet.


Copyright
=========

This document is hereby placed in the public domain by its author.


..
    Local Variables:
    mode: rst
    coding: utf-8
    End:
    vim: filetype=rst :



More information about the Python-Dev mailing list