[Python-checkins] r71032 - in python/trunk: Doc/library/unittest.rst Lib/test/test_unittest.py Lib/unittest.py Misc/NEWS

michael.foord python-checkins at python.org
Thu Apr 2 05:20:39 CEST 2009


Author: michael.foord
Date: Thu Apr  2 05:20:38 2009
New Revision: 71032

Log:
Better exception messages for unittest assert methods.

- unittest.assertNotEqual() now uses the inequality operator (!=) instead 
  of the equality operator.
  
- Default assertTrue and assertFalse messages are now useful. 

- TestCase has a longMessage attribute. This defaults to False, but if set to True 
  useful error messages are shown in addition to explicit messages passed to assert methods.

Issue #5663



Modified:
   python/trunk/Doc/library/unittest.rst
   python/trunk/Lib/test/test_unittest.py
   python/trunk/Lib/unittest.py
   python/trunk/Misc/NEWS

Modified: python/trunk/Doc/library/unittest.rst
==============================================================================
--- python/trunk/Doc/library/unittest.rst	(original)
+++ python/trunk/Doc/library/unittest.rst	Thu Apr  2 05:20:38 2009
@@ -1,4 +1,3 @@
-
 :mod:`unittest` --- Unit testing framework
 ==========================================
 
@@ -885,6 +884,25 @@
       fair" with the framework.  The initial value of this attribute is
       :exc:`AssertionError`.
 
+
+   .. attribute:: longMessage
+
+      If set to True then any explicit failure message you pass in to the
+      assert methods will be appended to the end of the normal failure message.
+      The normal messages contain useful information about the objects involved,
+      for example the message from assertEqual shows you the repr of the two
+      unequal objects. Setting this attribute to True allows you to have a
+      custom error message in addition to the normal one.
+
+      This attribute defaults to False, meaning that a custom message passed
+      to an assert method will silence the normal message.
+
+      The class setting can be overridden in individual tests by assigning an
+      instance attribute to True or False before calling the assert methods.
+
+      .. versionadded:: 2.7
+
+
    Testing frameworks can use the following methods to collect information on
    the test:
 

Modified: python/trunk/Lib/test/test_unittest.py
==============================================================================
--- python/trunk/Lib/test/test_unittest.py	(original)
+++ python/trunk/Lib/test/test_unittest.py	Thu Apr  2 05:20:38 2009
@@ -54,6 +54,8 @@
 
 
 class TestEquality(object):
+    """Used as a mixin for TestCase"""
+
     # Check for a valid __eq__ implementation
     def test_eq(self):
         for obj_1, obj_2 in self.eq_pairs:
@@ -67,6 +69,8 @@
             self.failIfEqual(obj_2, obj_1)
 
 class TestHashing(object):
+    """Used as a mixin for TestCase"""
+
     # Check for a valid __hash__ implementation
     def test_hash(self):
         for obj_1, obj_2 in self.eq_pairs:
@@ -2835,6 +2839,172 @@
             self.fail("assertRaises() didn't let exception pass through")
 
 
+class TestLongMessage(TestCase):
+    """Test that the individual asserts honour longMessage.
+    This actually tests all the message behaviour for
+    asserts that use longMessage."""
+
+    def setUp(self):
+        class TestableTestFalse(TestCase):
+            longMessage = False
+            failureException = self.failureException
+
+            def testTest(self):
+                pass
+
+        class TestableTestTrue(TestCase):
+            longMessage = True
+            failureException = self.failureException
+
+            def testTest(self):
+                pass
+
+        self.testableTrue = TestableTestTrue('testTest')
+        self.testableFalse = TestableTestFalse('testTest')
+
+    def testDefault(self):
+        self.assertFalse(TestCase.longMessage)
+
+    def test_formatMsg(self):
+        self.assertEquals(self.testableFalse._formatMessage(None, "foo"), "foo")
+        self.assertEquals(self.testableFalse._formatMessage("foo", "bar"), "foo")
+
+        self.assertEquals(self.testableTrue._formatMessage(None, "foo"), "foo")
+        self.assertEquals(self.testableTrue._formatMessage("foo", "bar"), "bar : foo")
+
+    def assertMessages(self, methodName, args, errors):
+        def getMethod(i):
+            useTestableFalse  = i < 2
+            if useTestableFalse:
+                test = self.testableFalse
+            else:
+                test = self.testableTrue
+            return getattr(test, methodName)
+
+        for i, expected_regexp in enumerate(errors):
+            testMethod = getMethod(i)
+            kwargs = {}
+            withMsg = i % 2
+            if withMsg:
+                kwargs = {"msg": "oops"}
+
+            with self.assertRaisesRegexp(self.failureException,
+                                         expected_regexp=expected_regexp):
+                testMethod(*args, **kwargs)
+
+    def testAssertTrue(self):
+        self.assertMessages('assertTrue', (False,),
+                            ["^False is not True$", "^oops$", "^False is not True$",
+                             "^False is not True : oops$"])
+
+    def testAssertFalse(self):
+        self.assertMessages('assertFalse', (True,),
+                            ["^True is not False$", "^oops$", "^True is not False$",
+                             "^True is not False : oops$"])
+
+    def testNotEqual(self):
+        self.assertMessages('assertNotEqual', (1, 1),
+                            ["^1 == 1$", "^oops$", "^1 == 1$",
+                             "^1 == 1 : oops$"])
+
+    def testAlmostEqual(self):
+        self.assertMessages('assertAlmostEqual', (1, 2),
+                            ["^1 != 2 within 7 places$", "^oops$",
+                             "^1 != 2 within 7 places$", "^1 != 2 within 7 places : oops$"])
+
+    def testNotAlmostEqual(self):
+        self.assertMessages('assertNotAlmostEqual', (1, 1),
+                            ["^1 == 1 within 7 places$", "^oops$",
+                             "^1 == 1 within 7 places$", "^1 == 1 within 7 places : oops$"])
+
+    def test_baseAssertEqual(self):
+        self.assertMessages('_baseAssertEqual', (1, 2),
+                            ["^1 != 2$", "^oops$", "^1 != 2$", "^1 != 2 : oops$"])
+
+    def testAssertSequenceEqual(self):
+        # Error messages are multiline so not testing on full message
+        # assertTupleEqual and assertListEqual delegate to this method
+        self.assertMessages('assertSequenceEqual', ([], [None]),
+                            ["\+ \[None\]$", "^oops$", r"\+ \[None\]$",
+                             r"\+ \[None\] : oops$"])
+
+    def testAssertSetEqual(self):
+        self.assertMessages('assertSetEqual', (set(), set([None])),
+                            ["None$", "^oops$", "None$",
+                             "None : oops$"])
+
+    def testAssertIn(self):
+        self.assertMessages('assertIn', (None, []),
+                            ['^None not found in \[\]$', "^oops$",
+                             '^None not found in \[\]$',
+                             '^None not found in \[\] : oops$'])
+
+    def testAssertNotIn(self):
+        self.assertMessages('assertNotIn', (None, [None]),
+                            ['^None unexpectedly found in \[None\]$', "^oops$",
+                             '^None unexpectedly found in \[None\]$',
+                             '^None unexpectedly found in \[None\] : oops$'])
+
+    def testAssertDictEqual(self):
+        self.assertMessages('assertDictEqual', ({}, {'key': 'value'}),
+                            [r"\+ \{'key': 'value'\}$", "^oops$",
+                             "\+ \{'key': 'value'\}$",
+                             "\+ \{'key': 'value'\} : oops$"])
+
+    def testAssertDictContainsSubset(self):
+        self.assertMessages('assertDictContainsSubset', ({'key': 'value'}, {}),
+                            ["^Missing: 'key'$", "^oops$",
+                             "^Missing: 'key'$",
+                             "^Missing: 'key' : oops$"])
+
+    def testAssertSameElements(self):
+        self.assertMessages('assertSameElements', ([], [None]),
+                            [r"\[None\]$", "^oops$",
+                             r"\[None\]$",
+                             r"\[None\] : oops$"])
+
+    def testAssertMultiLineEqual(self):
+        self.assertMessages('assertMultiLineEqual', ("", "foo"),
+                            [r"\+ foo$", "^oops$",
+                             r"\+ foo$",
+                             r"\+ foo : oops$"])
+
+    def testAssertLess(self):
+        self.assertMessages('assertLess', (2, 1),
+                            ["^2 not less than 1$", "^oops$",
+                             "^2 not less than 1$", "^2 not less than 1 : oops$"])
+
+    def testAssertLessEqual(self):
+        self.assertMessages('assertLessEqual', (2, 1),
+                            ["^2 not less than or equal to 1$", "^oops$",
+                             "^2 not less than or equal to 1$",
+                             "^2 not less than or equal to 1 : oops$"])
+
+    def testAssertGreater(self):
+        self.assertMessages('assertGreater', (1, 2),
+                            ["^1 not greater than 2$", "^oops$",
+                             "^1 not greater than 2$",
+                             "^1 not greater than 2 : oops$"])
+
+    def testAssertGreaterEqual(self):
+        self.assertMessages('assertGreaterEqual', (1, 2),
+                            ["^1 not greater than or equal to 2$", "^oops$",
+                             "^1 not greater than or equal to 2$",
+                             "^1 not greater than or equal to 2 : oops$"])
+
+    def testAssertIsNone(self):
+        self.assertMessages('assertIsNone', ('not None',),
+                            ["^'not None' is not None$", "^oops$",
+                             "^'not None' is not None$",
+                             "^'not None' is not None : oops$"])
+
+    def testAssertIsNotNone(self):
+        self.assertMessages('assertIsNotNone', (None,),
+                            ["^unexpectedly None$", "^oops$",
+                             "^unexpectedly None$",
+                             "^unexpectedly None : oops$"])
+
+
 ######################################################################
 ## Main
 ######################################################################
@@ -2842,7 +3012,7 @@
 def test_main():
     test_support.run_unittest(Test_TestCase, Test_TestLoader,
         Test_TestSuite, Test_TestResult, Test_FunctionTestCase,
-        Test_TestSkipping, Test_Assertions)
+        Test_TestSkipping, Test_Assertions, TestLongMessage)
 
 if __name__ == "__main__":
     test_main()

Modified: python/trunk/Lib/unittest.py
==============================================================================
--- python/trunk/Lib/unittest.py	(original)
+++ python/trunk/Lib/unittest.py	Thu Apr  2 05:20:38 2009
@@ -275,7 +275,7 @@
             raise self.failureException(
                 "{0} not raised".format(exc_name))
         if not issubclass(exc_type, self.expected):
-            # let unexpexted exceptions pass through
+            # let unexpected exceptions pass through
             return False
         if self.expected_regex is None:
             return True
@@ -318,6 +318,13 @@
 
     failureException = AssertionError
 
+    # This attribute determines whether long messages (including repr of
+    # objects used in assert methods) will be printed on failure in *addition*
+    # to any explicit message passed.
+
+    longMessage = False
+
+
     def __init__(self, methodName='runTest'):
         """Create an instance of the class that will use the named test
            method when executed. Raises a ValueError if the instance does
@@ -471,13 +478,32 @@
     def assertFalse(self, expr, msg=None):
         "Fail the test if the expression is true."
         if expr:
+            msg = self._formatMessage(msg, "%r is not False" % expr)
             raise self.failureException(msg)
 
     def assertTrue(self, expr, msg=None):
         """Fail the test unless the expression is true."""
         if not expr:
+            msg = self._formatMessage(msg, "%r is not True" % expr)
             raise self.failureException(msg)
 
+    def _formatMessage(self, msg, standardMsg):
+        """Honour the longMessage attribute when generating failure messages.
+        If longMessage is False this means:
+        * Use only an explicit message if it is provided
+        * Otherwise use the standard message for the assert
+
+        If longMessage is True:
+        * Use the standard message
+        * If an explicit message is provided, plus ' : ' and the explicit message
+        """
+        if not self.longMessage:
+            return msg or standardMsg
+        if msg is None:
+            return standardMsg
+        return standardMsg + ' : ' + msg
+
+
     def assertRaises(self, excClass, callableObj=None, *args, **kwargs):
         """Fail unless an exception of class excClass is thrown
            by callableObj when invoked with arguments args and keyword
@@ -523,7 +549,9 @@
     def _baseAssertEqual(self, first, second, msg=None):
         """The default assertEqual implementation, not type specific."""
         if not first == second:
-            raise self.failureException(msg or '%r != %r' % (first, second))
+            standardMsg = '%r != %r' % (first, second)
+            msg = self._formatMessage(msg, standardMsg)
+            raise self.failureException(msg)
 
     def assertEqual(self, first, second, msg=None):
         """Fail if the two objects are unequal as determined by the '=='
@@ -536,8 +564,9 @@
         """Fail if the two objects are equal as determined by the '=='
            operator.
         """
-        if first == second:
-            raise self.failureException(msg or '%r == %r' % (first, second))
+        if not first != second:
+            msg = self._formatMessage(msg, '%r == %r' % (first, second))
+            raise self.failureException(msg)
 
     def assertAlmostEqual(self, first, second, places=7, msg=None):
         """Fail if the two objects are unequal as determined by their
@@ -548,8 +577,9 @@
            as significant digits (measured from the most signficant digit).
         """
         if round(abs(second-first), places) != 0:
-            raise self.failureException(
-                  msg or '%r != %r within %r places' % (first, second, places))
+            standardMsg = '%r != %r within %r places' % (first, second, places)
+            msg = self._formatMessage(msg, standardMsg)
+            raise self.failureException(msg)
 
     def assertNotAlmostEqual(self, first, second, places=7, msg=None):
         """Fail if the two objects are equal as determined by their
@@ -560,8 +590,9 @@
            as significant digits (measured from the most signficant digit).
         """
         if round(abs(second-first), places) == 0:
-            raise self.failureException(
-                  msg or '%r == %r within %r places' % (first, second, places))
+            standardMsg = '%r == %r within %r places' % (first, second, places)
+            msg = self._formatMessage(msg, standardMsg)
+            raise self.failureException(msg)
 
     # Synonyms for assertion methods
 
@@ -680,10 +711,10 @@
                     except (TypeError, IndexError, NotImplementedError):
                         differing += ('Unable to index element %d '
                                       'of second %s\n' % (len1, seq_type_name))
-        if not msg:
-            msg = '\n'.join(difflib.ndiff(pprint.pformat(seq1).splitlines(),
-                                          pprint.pformat(seq2).splitlines()))
-        self.fail(differing + msg)
+        standardMsg = differing + '\n'.join(difflib.ndiff(pprint.pformat(seq1).splitlines(),
+                                            pprint.pformat(seq2).splitlines()))
+        msg = self._formatMessage(msg, standardMsg)
+        self.fail(msg)
 
     def assertListEqual(self, list1, list2, msg=None):
         """A list-specific equality assertion.
@@ -739,9 +770,6 @@
         if not (difference1 or difference2):
             return
 
-        if msg is not None:
-            self.fail(msg)
-
         lines = []
         if difference1:
             lines.append('Items in the first set but not the second:')
@@ -751,28 +779,31 @@
             lines.append('Items in the second set but not the first:')
             for item in difference2:
                 lines.append(repr(item))
-        self.fail('\n'.join(lines))
 
-    def assertIn(self, a, b, msg=None):
-        """Just like self.assert_(a in b), but with a nicer default message."""
-        if msg is None:
-            msg = '"%s" not found in "%s"' % (a, b)
-        self.assert_(a in b, msg)
+        standardMsg = '\n'.join(lines)
+        self.fail(self._formatMessage(msg, standardMsg))
 
-    def assertNotIn(self, a, b, msg=None):
-        """Just like self.assert_(a not in b), but with a nicer default message."""
-        if msg is None:
-            msg = '"%s" unexpectedly found in "%s"' % (a, b)
-        self.assert_(a not in b, msg)
+    def assertIn(self, member, container, msg=None):
+        """Just like self.assertTrue(a in b), but with a nicer default message."""
+        if member not in container:
+            standardMsg = '%r not found in %r' % (member, container)
+            self.fail(self._formatMessage(msg, standardMsg))
+
+    def assertNotIn(self, member, container, msg=None):
+        """Just like self.assertTrue(a not in b), but with a nicer default message."""
+        if member in container:
+            standardMsg = '%r unexpectedly found in %r' % (member, container)
+            self.fail(self._formatMessage(msg, standardMsg))
 
     def assertDictEqual(self, d1, d2, msg=None):
         self.assert_(isinstance(d1, dict), 'First argument is not a dictionary')
         self.assert_(isinstance(d2, dict), 'Second argument is not a dictionary')
 
         if d1 != d2:
-            self.fail(msg or ('\n' + '\n'.join(difflib.ndiff(
-                    pprint.pformat(d1).splitlines(),
-                    pprint.pformat(d2).splitlines()))))
+            standardMsg = ('\n' + '\n'.join(difflib.ndiff(
+                           pprint.pformat(d1).splitlines(),
+                           pprint.pformat(d2).splitlines())))
+            self.fail(self._formatMessage(msg, standardMsg))
 
     def assertDictContainsSubset(self, expected, actual, msg=None):
         """Checks whether actual is a superset of expected."""
@@ -782,23 +813,20 @@
             if key not in actual:
                 missing.append(key)
             elif value != actual[key]:
-                mismatched.append('%s, expected: %s, actual: %s' % (key, value,
-                                                                                                                        actual[key]))
+                mismatched.append('%s, expected: %s, actual: %s' % (key, value,                                                                                                       actual[key]))
 
         if not (missing or mismatched):
             return
 
-        missing_msg = mismatched_msg = ''
+        standardMsg = ''
         if missing:
-            missing_msg = 'Missing: %s' % ','.join(missing)
+            standardMsg = 'Missing: %r' % ','.join(missing)
         if mismatched:
-            mismatched_msg = 'Mismatched values: %s' % ','.join(mismatched)
+            if standardMsg:
+                standardMsg += '; '
+            standardMsg += 'Mismatched values: %s' % ','.join(mismatched)
 
-        if msg:
-            msg = '%s: %s; %s' % (msg, missing_msg, mismatched_msg)
-        else:
-            msg = '%s; %s' % (missing_msg, mismatched_msg)
-        self.fail(msg)
+        self.fail(self._formatMessage(msg, standardMsg))
 
     def assertSameElements(self, expected_seq, actual_seq, msg=None):
         """An unordered sequence specific comparison.
@@ -823,57 +851,59 @@
             missing, unexpected = _SortedListDifference(expected, actual)
         errors = []
         if missing:
-            errors.append('Expected, but missing:\n    %r\n' % missing)
+            errors.append('Expected, but missing:\n    %r' % missing)
         if unexpected:
-            errors.append('Unexpected, but present:\n    %r\n' % unexpected)
+            errors.append('Unexpected, but present:\n    %r' % unexpected)
         if errors:
-            self.fail(msg or ''.join(errors))
+            standardMsg = '\n'.join(errors)
+            self.fail(self._formatMessage(msg, standardMsg))
 
     def assertMultiLineEqual(self, first, second, msg=None):
         """Assert that two multi-line strings are equal."""
-        self.assert_(isinstance(first, types.StringTypes), (
+        self.assert_(isinstance(first, basestring), (
                 'First argument is not a string'))
-        self.assert_(isinstance(second, types.StringTypes), (
+        self.assert_(isinstance(second, basestring), (
                 'Second argument is not a string'))
 
         if first != second:
-            raise self.failureException(
-                    msg or '\n' + ''.join(difflib.ndiff(first.splitlines(True),
-                                                                                            second.splitlines(True))))
+            standardMsg = '\n' + ''.join(difflib.ndiff(first.splitlines(True), second.splitlines(True)))
+            self.fail(self._formatMessage(msg, standardMsg))
 
     def assertLess(self, a, b, msg=None):
-        """Just like self.assert_(a < b), but with a nicer default message."""
-        if msg is None:
-            msg = '"%r" unexpectedly not less than "%r"' % (a, b)
-        self.assert_(a < b, msg)
+        """Just like self.assertTrue(a < b), but with a nicer default message."""
+        if not a < b:
+            standardMsg = '%r not less than %r' % (a, b)
+            self.fail(self._formatMessage(msg, standardMsg))
 
     def assertLessEqual(self, a, b, msg=None):
-        """Just like self.assert_(a <= b), but with a nicer default message."""
-        if msg is None:
-            msg = '"%r" unexpectedly not less than or equal to "%r"' % (a, b)
-        self.assert_(a <= b, msg)
+        """Just like self.assertTrue(a <= b), but with a nicer default message."""
+        if not a <= b:
+            standardMsg = '%r not less than or equal to %r' % (a, b)
+            self.fail(self._formatMessage(msg, standardMsg))
 
     def assertGreater(self, a, b, msg=None):
-        """Just like self.assert_(a > b), but with a nicer default message."""
-        if msg is None:
-            msg = '"%r" unexpectedly not greater than "%r"' % (a, b)
-        self.assert_(a > b, msg)
+        """Just like self.assertTrue(a > b), but with a nicer default message."""
+        if not a > b:
+            standardMsg = '%r not greater than %r' % (a, b)
+            self.fail(self._formatMessage(msg, standardMsg))
 
     def assertGreaterEqual(self, a, b, msg=None):
-        """Just like self.assert_(a >= b), but with a nicer default message."""
-        if msg is None:
-            msg = '"%r" unexpectedly not greater than or equal to "%r"' % (a, b)
-        self.assert_(a >= b, msg)
+        """Just like self.assertTrue(a >= b), but with a nicer default message."""
+        if not a >= b:
+            standardMsg = '%r not greater than or equal to %r' % (a, b)
+            self.fail(self._formatMessage(msg, standardMsg))
 
     def assertIsNone(self, obj, msg=None):
-        """Same as self.assert_(obj is None), with a nicer default message."""
-        if msg is None:
-            msg = '"%s" unexpectedly not None' % obj
-        self.assert_(obj is None, msg)
+        """Same as self.assertTrue(obj is None), with a nicer default message."""
+        if obj is not None:
+            standardMsg = '%r is not None' % obj
+            self.fail(self._formatMessage(msg, standardMsg))
 
-    def assertIsNotNone(self, obj, msg='unexpectedly None'):
+    def assertIsNotNone(self, obj, msg=None):
         """Included for symmetry with assertIsNone."""
-        self.assert_(obj is not None, msg)
+        if obj is None:
+            standardMsg = 'unexpectedly None'
+            self.fail(self._formatMessage(msg, standardMsg))
 
     def assertRaisesRegexp(self, expected_exception, expected_regexp,
                            callable_obj=None, *args, **kwargs):

Modified: python/trunk/Misc/NEWS
==============================================================================
--- python/trunk/Misc/NEWS	(original)
+++ python/trunk/Misc/NEWS	Thu Apr  2 05:20:38 2009
@@ -202,6 +202,14 @@
 Library
 -------
 
+- unittest.assertNotEqual() now uses the inequality operator (!=) instead 
+  of the equality operator.
+  
+- Issue #5663: better failure messages for unittest asserts. Default assertTrue
+  and assertFalse messages are now useful. TestCase has a longMessage attribute.
+  This defaults to False, but if set to True useful error messages are shown in
+  addition to explicit messages passed to assert methods.
+  
 - Issue #3110: Add additional protect around SEM_VALUE_MAX for multiprocessing
 
 - In Pdb, prevent the reassignment of __builtin__._ by sys.displayhook on


More information about the Python-checkins mailing list