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
diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst
index a03982a..38e4239 100644
--- a/Doc/library/unittest.rst
+++ b/Doc/library/unittest.rst
@@ -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:
diff --git a/Lib/test/test_unittest.py b/Lib/test/test_unittest.py
index 25fe592..c16327e 100644
--- a/Lib/test/test_unittest.py
+++ b/Lib/test/test_unittest.py
@@ -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()
diff --git a/Lib/unittest.py b/Lib/unittest.py
index c355f8f..b6b96b3 100644
--- a/Lib/unittest.py
+++ b/Lib/unittest.py
@@ -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):
diff --git a/Misc/NEWS b/Misc/NEWS
index 036a061..dd8c0ad 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -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