gh-71339: Add additional assertion methods for unittest (GH-128707)

Add the following methods:

* assertHasAttr() and assertNotHasAttr()
* assertIsSubclass() and assertNotIsSubclass()
* assertStartsWith() and assertNotStartsWith()
* assertEndsWith() and assertNotEndsWith()

Also improve error messages for assertIsInstance() and
assertNotIsInstance().
This commit is contained in:
Serhiy Storchaka 2025-01-14 10:02:38 +02:00 committed by GitHub
parent 41f73501ec
commit 06cad77a5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 555 additions and 100 deletions

View file

@ -883,6 +883,12 @@ Test cases
| :meth:`assertNotIsInstance(a, b) | ``not isinstance(a, b)`` | 3.2 | | :meth:`assertNotIsInstance(a, b) | ``not isinstance(a, b)`` | 3.2 |
| <TestCase.assertNotIsInstance>` | | | | <TestCase.assertNotIsInstance>` | | |
+-----------------------------------------+-----------------------------+---------------+ +-----------------------------------------+-----------------------------+---------------+
| :meth:`assertIsSubclass(a, b) | ``issubclass(a, b)`` | 3.14 |
| <TestCase.assertIsSubclass>` | | |
+-----------------------------------------+-----------------------------+---------------+
| :meth:`assertNotIsSubclass(a, b) | ``not issubclass(a, b)`` | 3.14 |
| <TestCase.assertNotIsSubclass>` | | |
+-----------------------------------------+-----------------------------+---------------+
All the assert methods accept a *msg* argument that, if specified, is used All the assert methods accept a *msg* argument that, if specified, is used
as the error message on failure (see also :data:`longMessage`). as the error message on failure (see also :data:`longMessage`).
@ -961,6 +967,15 @@ Test cases
.. versionadded:: 3.2 .. versionadded:: 3.2
.. method:: assertIsSubclass(cls, superclass, msg=None)
assertNotIsSubclass(cls, superclass, msg=None)
Test that *cls* is (or is not) a subclass of *superclass* (which can be a
class or a tuple of classes, as supported by :func:`issubclass`).
To check for the exact type, use :func:`assertIs(cls, superclass) <assertIs>`.
.. versionadded:: next
It is also possible to check the production of exceptions, warnings, and It is also possible to check the production of exceptions, warnings, and
log messages using the following methods: log messages using the following methods:
@ -1210,6 +1225,24 @@ Test cases
| <TestCase.assertCountEqual>` | elements in the same number, | | | <TestCase.assertCountEqual>` | elements in the same number, | |
| | regardless of their order. | | | | regardless of their order. | |
+---------------------------------------+--------------------------------+--------------+ +---------------------------------------+--------------------------------+--------------+
| :meth:`assertStartsWith(a, b) | ``a.startswith(b)`` | 3.14 |
| <TestCase.assertStartsWith>` | | |
+---------------------------------------+--------------------------------+--------------+
| :meth:`assertNotStartsWith(a, b) | ``not a.startswith(b)`` | 3.14 |
| <TestCase.assertNotStartsWith>` | | |
+---------------------------------------+--------------------------------+--------------+
| :meth:`assertEndsWith(a, b) | ``a.endswith(b)`` | 3.14 |
| <TestCase.assertEndsWith>` | | |
+---------------------------------------+--------------------------------+--------------+
| :meth:`assertNotEndsWith(a, b) | ``not a.endswith(b)`` | 3.14 |
| <TestCase.assertNotEndsWith>` | | |
+---------------------------------------+--------------------------------+--------------+
| :meth:`assertHasAttr(a, b) | ``hastattr(a, b)`` | 3.14 |
| <TestCase.assertHasAttr>` | | |
+---------------------------------------+--------------------------------+--------------+
| :meth:`assertNotHasAttr(a, b) | ``not hastattr(a, b)`` | 3.14 |
| <TestCase.assertNotHasAttr>` | | |
+---------------------------------------+--------------------------------+--------------+
.. method:: assertAlmostEqual(first, second, places=7, msg=None, delta=None) .. method:: assertAlmostEqual(first, second, places=7, msg=None, delta=None)
@ -1279,6 +1312,34 @@ Test cases
.. versionadded:: 3.2 .. versionadded:: 3.2
.. method:: assertStartsWith(s, prefix, msg=None)
.. method:: assertNotStartsWith(s, prefix, msg=None)
Test that the Unicode or byte string *s* starts (or does not start)
with a *prefix*.
*prefix* can also be a tuple of strings to try.
.. versionadded:: next
.. method:: assertEndsWith(s, suffix, msg=None)
.. method:: assertNotEndsWith(s, suffix, msg=None)
Test that the Unicode or byte string *s* ends (or does not end)
with a *suffix*.
*suffix* can also be a tuple of strings to try.
.. versionadded:: next
.. method:: assertHasAttr(obj, name, msg=None)
.. method:: assertNotHasAttr(obj, name, msg=None)
Test that the object *obj* has (or has not) an attribute *name*.
.. versionadded:: next
.. _type-specific-methods: .. _type-specific-methods:
The :meth:`assertEqual` method dispatches the equality check for objects of The :meth:`assertEqual` method dispatches the equality check for objects of

View file

@ -670,6 +670,23 @@ unittest
directory again. It was removed in Python 3.11. directory again. It was removed in Python 3.11.
(Contributed by Jacob Walls in :gh:`80958`.) (Contributed by Jacob Walls in :gh:`80958`.)
* A number of new methods were added in the :class:`~unittest.TestCase` class
that provide more specialized tests.
- :meth:`~unittest.TestCase.assertHasAttr` and
:meth:`~unittest.TestCase.assertNotHasAttr` check whether the object
has a particular attribute.
- :meth:`~unittest.TestCase.assertIsSubclass` and
:meth:`~unittest.TestCase.assertNotIsSubclass` check whether the object
is a subclass of a particular class, or of one of a tuple of classes.
- :meth:`~unittest.TestCase.assertStartsWith`,
:meth:`~unittest.TestCase.assertNotStartsWith`,
:meth:`~unittest.TestCase.assertEndsWith` and
:meth:`~unittest.TestCase.assertNotEndsWith` check whether the Unicode
or byte string starts or ends with particular string(s).
(Contributed by Serhiy Storchaka in :gh:`71339`.)
urllib urllib
------ ------

View file

@ -405,14 +405,6 @@ class OperatorsTest(unittest.TestCase):
class ClassPropertiesAndMethods(unittest.TestCase): class ClassPropertiesAndMethods(unittest.TestCase):
def assertHasAttr(self, obj, name):
self.assertTrue(hasattr(obj, name),
'%r has no attribute %r' % (obj, name))
def assertNotHasAttr(self, obj, name):
self.assertFalse(hasattr(obj, name),
'%r has unexpected attribute %r' % (obj, name))
def test_python_dicts(self): def test_python_dicts(self):
# Testing Python subclass of dict... # Testing Python subclass of dict...
self.assertTrue(issubclass(dict, dict)) self.assertTrue(issubclass(dict, dict))

View file

@ -280,11 +280,6 @@ class DebuggerTests(unittest.TestCase):
return out return out
def assertEndsWith(self, actual, exp_end):
'''Ensure that the given "actual" string ends with "exp_end"'''
self.assertTrue(actual.endswith(exp_end),
msg='%r did not end with %r' % (actual, exp_end))
def assertMultilineMatches(self, actual, pattern): def assertMultilineMatches(self, actual, pattern):
m = re.match(pattern, actual, re.DOTALL) m = re.match(pattern, actual, re.DOTALL)
if not m: if not m:

View file

@ -43,12 +43,6 @@ class FunctionalAPIBase(util.DiskSetup):
with self.subTest(path_parts=path_parts): with self.subTest(path_parts=path_parts):
yield path_parts yield path_parts
def assertEndsWith(self, string, suffix):
"""Assert that `string` ends with `suffix`.
Used to ignore an architecture-specific UTF-16 byte-order mark."""
self.assertEqual(string[-len(suffix) :], suffix)
def test_read_text(self): def test_read_text(self):
self.assertEqual( self.assertEqual(
resources.read_text(self.anchor01, 'utf-8.file'), resources.read_text(self.anchor01, 'utf-8.file'),

View file

@ -31,14 +31,6 @@ class PyclbrTest(TestCase):
print("l1=%r\nl2=%r\nignore=%r" % (l1, l2, ignore), file=sys.stderr) print("l1=%r\nl2=%r\nignore=%r" % (l1, l2, ignore), file=sys.stderr)
self.fail("%r missing" % missing.pop()) self.fail("%r missing" % missing.pop())
def assertHasattr(self, obj, attr, ignore):
''' succeed iff hasattr(obj,attr) or attr in ignore. '''
if attr in ignore: return
if not hasattr(obj, attr): print("???", attr)
self.assertTrue(hasattr(obj, attr),
'expected hasattr(%r, %r)' % (obj, attr))
def assertHaskey(self, obj, key, ignore): def assertHaskey(self, obj, key, ignore):
''' succeed iff key in obj or key in ignore. ''' ''' succeed iff key in obj or key in ignore. '''
if key in ignore: return if key in ignore: return
@ -86,7 +78,7 @@ class PyclbrTest(TestCase):
for name, value in dict.items(): for name, value in dict.items():
if name in ignore: if name in ignore:
continue continue
self.assertHasattr(module, name, ignore) self.assertHasAttr(module, name, ignore)
py_item = getattr(module, name) py_item = getattr(module, name)
if isinstance(value, pyclbr.Function): if isinstance(value, pyclbr.Function):
self.assertIsInstance(py_item, (FunctionType, BuiltinFunctionType)) self.assertIsInstance(py_item, (FunctionType, BuiltinFunctionType))

View file

@ -59,20 +59,6 @@ CANNOT_SUBCLASS_INSTANCE = 'Cannot subclass an instance of %s'
class BaseTestCase(TestCase): class BaseTestCase(TestCase):
def assertIsSubclass(self, cls, class_or_tuple, msg=None):
if not issubclass(cls, class_or_tuple):
message = '%r is not a subclass of %r' % (cls, class_or_tuple)
if msg is not None:
message += ' : %s' % msg
raise self.failureException(message)
def assertNotIsSubclass(self, cls, class_or_tuple, msg=None):
if issubclass(cls, class_or_tuple):
message = '%r is a subclass of %r' % (cls, class_or_tuple)
if msg is not None:
message += ' : %s' % msg
raise self.failureException(message)
def clear_caches(self): def clear_caches(self):
for f in typing._cleanups: for f in typing._cleanups:
f() f()
@ -1252,10 +1238,6 @@ class UnpackTests(BaseTestCase):
class TypeVarTupleTests(BaseTestCase): class TypeVarTupleTests(BaseTestCase):
def assertEndsWith(self, string, tail):
if not string.endswith(tail):
self.fail(f"String {string!r} does not end with {tail!r}")
def test_name(self): def test_name(self):
Ts = TypeVarTuple('Ts') Ts = TypeVarTuple('Ts')
self.assertEqual(Ts.__name__, 'Ts') self.assertEqual(Ts.__name__, 'Ts')

View file

@ -10,6 +10,7 @@ import weakref
import inspect import inspect
import types import types
from collections import UserString
from copy import deepcopy from copy import deepcopy
from test import support from test import support
@ -54,6 +55,10 @@ class Test(object):
self.events.append('tearDown') self.events.append('tearDown')
class List(list):
pass
class Test_TestCase(unittest.TestCase, TestEquality, TestHashing): class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
### Set up attributes used by inherited tests ### Set up attributes used by inherited tests
@ -85,7 +90,7 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
def runTest(self): raise MyException() def runTest(self): raise MyException()
def test(self): pass def test(self): pass
self.assertEqual(Test().id()[-13:], '.Test.runTest') self.assertEndsWith(Test().id(), '.Test.runTest')
# test that TestCase can be instantiated with no args # test that TestCase can be instantiated with no args
# primarily for use at the interactive interpreter # primarily for use at the interactive interpreter
@ -106,7 +111,7 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
def runTest(self): raise MyException() def runTest(self): raise MyException()
def test(self): pass def test(self): pass
self.assertEqual(Test('test').id()[-10:], '.Test.test') self.assertEndsWith(Test('test').id(), '.Test.test')
# "class TestCase([methodName])" # "class TestCase([methodName])"
# ... # ...
@ -700,16 +705,120 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
self.assertRaises(self.failureException, self.assertIsNot, thing, thing) self.assertRaises(self.failureException, self.assertIsNot, thing, thing)
def testAssertIsInstance(self): def testAssertIsInstance(self):
thing = [] thing = List()
self.assertIsInstance(thing, list) self.assertIsInstance(thing, list)
self.assertRaises(self.failureException, self.assertIsInstance, self.assertIsInstance(thing, (int, list))
thing, dict) with self.assertRaises(self.failureException) as cm:
self.assertIsInstance(thing, int)
self.assertEqual(str(cm.exception),
"[] is not an instance of <class 'int'>")
with self.assertRaises(self.failureException) as cm:
self.assertIsInstance(thing, (int, float))
self.assertEqual(str(cm.exception),
"[] is not an instance of any of (<class 'int'>, <class 'float'>)")
with self.assertRaises(self.failureException) as cm:
self.assertIsInstance(thing, int, 'ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
with self.assertRaises(self.failureException) as cm:
self.assertIsInstance(thing, int, msg='ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
def testAssertNotIsInstance(self): def testAssertNotIsInstance(self):
thing = [] thing = List()
self.assertNotIsInstance(thing, dict) self.assertNotIsInstance(thing, int)
self.assertRaises(self.failureException, self.assertNotIsInstance, self.assertNotIsInstance(thing, (int, float))
thing, list) with self.assertRaises(self.failureException) as cm:
self.assertNotIsInstance(thing, list)
self.assertEqual(str(cm.exception),
"[] is an instance of <class 'list'>")
with self.assertRaises(self.failureException) as cm:
self.assertNotIsInstance(thing, (int, list))
self.assertEqual(str(cm.exception),
"[] is an instance of <class 'list'>")
with self.assertRaises(self.failureException) as cm:
self.assertNotIsInstance(thing, list, 'ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
with self.assertRaises(self.failureException) as cm:
self.assertNotIsInstance(thing, list, msg='ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
def testAssertIsSubclass(self):
self.assertIsSubclass(List, list)
self.assertIsSubclass(List, (int, list))
with self.assertRaises(self.failureException) as cm:
self.assertIsSubclass(List, int)
self.assertEqual(str(cm.exception),
f"{List!r} is not a subclass of <class 'int'>")
with self.assertRaises(self.failureException) as cm:
self.assertIsSubclass(List, (int, float))
self.assertEqual(str(cm.exception),
f"{List!r} is not a subclass of any of (<class 'int'>, <class 'float'>)")
with self.assertRaises(self.failureException) as cm:
self.assertIsSubclass(1, int)
self.assertEqual(str(cm.exception), "1 is not a class")
with self.assertRaises(self.failureException) as cm:
self.assertIsSubclass(List, int, 'ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
with self.assertRaises(self.failureException) as cm:
self.assertIsSubclass(List, int, msg='ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
def testAssertNotIsSubclass(self):
self.assertNotIsSubclass(List, int)
self.assertNotIsSubclass(List, (int, float))
with self.assertRaises(self.failureException) as cm:
self.assertNotIsSubclass(List, list)
self.assertEqual(str(cm.exception),
f"{List!r} is a subclass of <class 'list'>")
with self.assertRaises(self.failureException) as cm:
self.assertNotIsSubclass(List, (int, list))
self.assertEqual(str(cm.exception),
f"{List!r} is a subclass of <class 'list'>")
with self.assertRaises(self.failureException) as cm:
self.assertNotIsSubclass(1, int)
self.assertEqual(str(cm.exception), "1 is not a class")
with self.assertRaises(self.failureException) as cm:
self.assertNotIsSubclass(List, list, 'ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
with self.assertRaises(self.failureException) as cm:
self.assertNotIsSubclass(List, list, msg='ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
def testAssertHasAttr(self):
a = List()
a.x = 1
self.assertHasAttr(a, 'x')
with self.assertRaises(self.failureException) as cm:
self.assertHasAttr(a, 'y')
self.assertEqual(str(cm.exception),
"List instance has no attribute 'y'")
with self.assertRaises(self.failureException) as cm:
self.assertHasAttr(a, 'y', 'ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
with self.assertRaises(self.failureException) as cm:
self.assertHasAttr(a, 'y', msg='ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
def testAssertNotHasAttr(self):
a = List()
a.x = 1
self.assertNotHasAttr(a, 'y')
with self.assertRaises(self.failureException) as cm:
self.assertNotHasAttr(a, 'x')
self.assertEqual(str(cm.exception),
"List instance has unexpected attribute 'x'")
with self.assertRaises(self.failureException) as cm:
self.assertNotHasAttr(a, 'x', 'ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
with self.assertRaises(self.failureException) as cm:
self.assertNotHasAttr(a, 'x', msg='ababahalamaha')
self.assertIn('ababahalamaha', str(cm.exception))
def testAssertIn(self): def testAssertIn(self):
animals = {'monkey': 'banana', 'cow': 'grass', 'seal': 'fish'} animals = {'monkey': 'banana', 'cow': 'grass', 'seal': 'fish'}
@ -1864,6 +1973,186 @@ test case
pass pass
self.assertIsNone(value) self.assertIsNone(value)
def testAssertStartswith(self):
self.assertStartsWith('ababahalamaha', 'ababa')
self.assertStartsWith('ababahalamaha', ('x', 'ababa', 'y'))
self.assertStartsWith(UserString('ababahalamaha'), 'ababa')
self.assertStartsWith(UserString('ababahalamaha'), ('x', 'ababa', 'y'))
self.assertStartsWith(bytearray(b'ababahalamaha'), b'ababa')
self.assertStartsWith(bytearray(b'ababahalamaha'), (b'x', b'ababa', b'y'))
self.assertStartsWith(b'ababahalamaha', bytearray(b'ababa'))
self.assertStartsWith(b'ababahalamaha',
(bytearray(b'x'), bytearray(b'ababa'), bytearray(b'y')))
with self.assertRaises(self.failureException) as cm:
self.assertStartsWith('ababahalamaha', 'amaha')
self.assertEqual(str(cm.exception),
"'ababahalamaha' doesn't start with 'amaha'")
with self.assertRaises(self.failureException) as cm:
self.assertStartsWith('ababahalamaha', ('x', 'y'))
self.assertEqual(str(cm.exception),
"'ababahalamaha' doesn't start with any of ('x', 'y')")
with self.assertRaises(self.failureException) as cm:
self.assertStartsWith(b'ababahalamaha', 'ababa')
self.assertEqual(str(cm.exception), 'Expected str, not bytes')
with self.assertRaises(self.failureException) as cm:
self.assertStartsWith(b'ababahalamaha', ('amaha', 'ababa'))
self.assertEqual(str(cm.exception), 'Expected str, not bytes')
with self.assertRaises(self.failureException) as cm:
self.assertStartsWith([], 'ababa')
self.assertEqual(str(cm.exception), 'Expected str, not list')
with self.assertRaises(self.failureException) as cm:
self.assertStartsWith('ababahalamaha', b'ababa')
self.assertEqual(str(cm.exception), 'Expected bytes, not str')
with self.assertRaises(self.failureException) as cm:
self.assertStartsWith('ababahalamaha', (b'amaha', b'ababa'))
self.assertEqual(str(cm.exception), 'Expected bytes, not str')
with self.assertRaises(TypeError):
self.assertStartsWith('ababahalamaha', ord('a'))
with self.assertRaises(self.failureException) as cm:
self.assertStartsWith('ababahalamaha', 'amaha', 'abracadabra')
self.assertIn('ababahalamaha', str(cm.exception))
with self.assertRaises(self.failureException) as cm:
self.assertStartsWith('ababahalamaha', 'amaha', msg='abracadabra')
self.assertIn('ababahalamaha', str(cm.exception))
def testAssertNotStartswith(self):
self.assertNotStartsWith('ababahalamaha', 'amaha')
self.assertNotStartsWith('ababahalamaha', ('x', 'amaha', 'y'))
self.assertNotStartsWith(UserString('ababahalamaha'), 'amaha')
self.assertNotStartsWith(UserString('ababahalamaha'), ('x', 'amaha', 'y'))
self.assertNotStartsWith(bytearray(b'ababahalamaha'), b'amaha')
self.assertNotStartsWith(bytearray(b'ababahalamaha'), (b'x', b'amaha', b'y'))
self.assertNotStartsWith(b'ababahalamaha', bytearray(b'amaha'))
self.assertNotStartsWith(b'ababahalamaha',
(bytearray(b'x'), bytearray(b'amaha'), bytearray(b'y')))
with self.assertRaises(self.failureException) as cm:
self.assertNotStartsWith('ababahalamaha', 'ababa')
self.assertEqual(str(cm.exception),
"'ababahalamaha' starts with 'ababa'")
with self.assertRaises(self.failureException) as cm:
self.assertNotStartsWith('ababahalamaha', ('x', 'ababa', 'y'))
self.assertEqual(str(cm.exception),
"'ababahalamaha' starts with 'ababa'")
with self.assertRaises(self.failureException) as cm:
self.assertNotStartsWith(b'ababahalamaha', 'ababa')
self.assertEqual(str(cm.exception), 'Expected str, not bytes')
with self.assertRaises(self.failureException) as cm:
self.assertNotStartsWith(b'ababahalamaha', ('amaha', 'ababa'))
self.assertEqual(str(cm.exception), 'Expected str, not bytes')
with self.assertRaises(self.failureException) as cm:
self.assertNotStartsWith([], 'ababa')
self.assertEqual(str(cm.exception), 'Expected str, not list')
with self.assertRaises(self.failureException) as cm:
self.assertNotStartsWith('ababahalamaha', b'ababa')
self.assertEqual(str(cm.exception), 'Expected bytes, not str')
with self.assertRaises(self.failureException) as cm:
self.assertNotStartsWith('ababahalamaha', (b'amaha', b'ababa'))
self.assertEqual(str(cm.exception), 'Expected bytes, not str')
with self.assertRaises(TypeError):
self.assertNotStartsWith('ababahalamaha', ord('a'))
with self.assertRaises(self.failureException) as cm:
self.assertNotStartsWith('ababahalamaha', 'ababa', 'abracadabra')
self.assertIn('ababahalamaha', str(cm.exception))
with self.assertRaises(self.failureException) as cm:
self.assertNotStartsWith('ababahalamaha', 'ababa', msg='abracadabra')
self.assertIn('ababahalamaha', str(cm.exception))
def testAssertEndswith(self):
self.assertEndsWith('ababahalamaha', 'amaha')
self.assertEndsWith('ababahalamaha', ('x', 'amaha', 'y'))
self.assertEndsWith(UserString('ababahalamaha'), 'amaha')
self.assertEndsWith(UserString('ababahalamaha'), ('x', 'amaha', 'y'))
self.assertEndsWith(bytearray(b'ababahalamaha'), b'amaha')
self.assertEndsWith(bytearray(b'ababahalamaha'), (b'x', b'amaha', b'y'))
self.assertEndsWith(b'ababahalamaha', bytearray(b'amaha'))
self.assertEndsWith(b'ababahalamaha',
(bytearray(b'x'), bytearray(b'amaha'), bytearray(b'y')))
with self.assertRaises(self.failureException) as cm:
self.assertEndsWith('ababahalamaha', 'ababa')
self.assertEqual(str(cm.exception),
"'ababahalamaha' doesn't end with 'ababa'")
with self.assertRaises(self.failureException) as cm:
self.assertEndsWith('ababahalamaha', ('x', 'y'))
self.assertEqual(str(cm.exception),
"'ababahalamaha' doesn't end with any of ('x', 'y')")
with self.assertRaises(self.failureException) as cm:
self.assertEndsWith(b'ababahalamaha', 'amaha')
self.assertEqual(str(cm.exception), 'Expected str, not bytes')
with self.assertRaises(self.failureException) as cm:
self.assertEndsWith(b'ababahalamaha', ('ababa', 'amaha'))
self.assertEqual(str(cm.exception), 'Expected str, not bytes')
with self.assertRaises(self.failureException) as cm:
self.assertEndsWith([], 'amaha')
self.assertEqual(str(cm.exception), 'Expected str, not list')
with self.assertRaises(self.failureException) as cm:
self.assertEndsWith('ababahalamaha', b'amaha')
self.assertEqual(str(cm.exception), 'Expected bytes, not str')
with self.assertRaises(self.failureException) as cm:
self.assertEndsWith('ababahalamaha', (b'ababa', b'amaha'))
self.assertEqual(str(cm.exception), 'Expected bytes, not str')
with self.assertRaises(TypeError):
self.assertEndsWith('ababahalamaha', ord('a'))
with self.assertRaises(self.failureException) as cm:
self.assertEndsWith('ababahalamaha', 'ababa', 'abracadabra')
self.assertIn('ababahalamaha', str(cm.exception))
with self.assertRaises(self.failureException) as cm:
self.assertEndsWith('ababahalamaha', 'ababa', msg='abracadabra')
self.assertIn('ababahalamaha', str(cm.exception))
def testAssertNotEndswith(self):
self.assertNotEndsWith('ababahalamaha', 'ababa')
self.assertNotEndsWith('ababahalamaha', ('x', 'ababa', 'y'))
self.assertNotEndsWith(UserString('ababahalamaha'), 'ababa')
self.assertNotEndsWith(UserString('ababahalamaha'), ('x', 'ababa', 'y'))
self.assertNotEndsWith(bytearray(b'ababahalamaha'), b'ababa')
self.assertNotEndsWith(bytearray(b'ababahalamaha'), (b'x', b'ababa', b'y'))
self.assertNotEndsWith(b'ababahalamaha', bytearray(b'ababa'))
self.assertNotEndsWith(b'ababahalamaha',
(bytearray(b'x'), bytearray(b'ababa'), bytearray(b'y')))
with self.assertRaises(self.failureException) as cm:
self.assertNotEndsWith('ababahalamaha', 'amaha')
self.assertEqual(str(cm.exception),
"'ababahalamaha' ends with 'amaha'")
with self.assertRaises(self.failureException) as cm:
self.assertNotEndsWith('ababahalamaha', ('x', 'amaha', 'y'))
self.assertEqual(str(cm.exception),
"'ababahalamaha' ends with 'amaha'")
with self.assertRaises(self.failureException) as cm:
self.assertNotEndsWith(b'ababahalamaha', 'amaha')
self.assertEqual(str(cm.exception), 'Expected str, not bytes')
with self.assertRaises(self.failureException) as cm:
self.assertNotEndsWith(b'ababahalamaha', ('ababa', 'amaha'))
self.assertEqual(str(cm.exception), 'Expected str, not bytes')
with self.assertRaises(self.failureException) as cm:
self.assertNotEndsWith([], 'amaha')
self.assertEqual(str(cm.exception), 'Expected str, not list')
with self.assertRaises(self.failureException) as cm:
self.assertNotEndsWith('ababahalamaha', b'amaha')
self.assertEqual(str(cm.exception), 'Expected bytes, not str')
with self.assertRaises(self.failureException) as cm:
self.assertNotEndsWith('ababahalamaha', (b'ababa', b'amaha'))
self.assertEqual(str(cm.exception), 'Expected bytes, not str')
with self.assertRaises(TypeError):
self.assertNotEndsWith('ababahalamaha', ord('a'))
with self.assertRaises(self.failureException) as cm:
self.assertNotEndsWith('ababahalamaha', 'amaha', 'abracadabra')
self.assertIn('ababahalamaha', str(cm.exception))
with self.assertRaises(self.failureException) as cm:
self.assertNotEndsWith('ababahalamaha', 'amaha', msg='abracadabra')
self.assertIn('ababahalamaha', str(cm.exception))
def testDeprecatedFailMethods(self): def testDeprecatedFailMethods(self):
"""Test that the deprecated fail* methods get removed in 3.12""" """Test that the deprecated fail* methods get removed in 3.12"""
deprecated_names = [ deprecated_names = [

View file

@ -76,7 +76,7 @@ class Test_TestLoader(unittest.TestCase):
loader = unittest.TestLoader() loader = unittest.TestLoader()
# This has to be false for the test to succeed # This has to be false for the test to succeed
self.assertFalse('runTest'.startswith(loader.testMethodPrefix)) self.assertNotStartsWith('runTest', loader.testMethodPrefix)
suite = loader.loadTestsFromTestCase(Foo) suite = loader.loadTestsFromTestCase(Foo)
self.assertIsInstance(suite, loader.suiteClass) self.assertIsInstance(suite, loader.suiteClass)

View file

@ -128,14 +128,14 @@ class Test_TestProgram(unittest.TestCase):
argv=["foobar"], argv=["foobar"],
testRunner=unittest.TextTestRunner(stream=stream), testRunner=unittest.TextTestRunner(stream=stream),
testLoader=self.TestLoader(self.FooBar)) testLoader=self.TestLoader(self.FooBar))
self.assertTrue(hasattr(program, 'result')) self.assertHasAttr(program, 'result')
out = stream.getvalue() out = stream.getvalue()
self.assertIn('\nFAIL: testFail ', out) self.assertIn('\nFAIL: testFail ', out)
self.assertIn('\nERROR: testError ', out) self.assertIn('\nERROR: testError ', out)
self.assertIn('\nUNEXPECTED SUCCESS: testUnexpectedSuccess ', out) self.assertIn('\nUNEXPECTED SUCCESS: testUnexpectedSuccess ', out)
expected = ('\n\nFAILED (failures=1, errors=1, skipped=1, ' expected = ('\n\nFAILED (failures=1, errors=1, skipped=1, '
'expected failures=1, unexpected successes=1)\n') 'expected failures=1, unexpected successes=1)\n')
self.assertTrue(out.endswith(expected)) self.assertEndsWith(out, expected)
@force_not_colorized @force_not_colorized
def test_Exit(self): def test_Exit(self):
@ -153,7 +153,7 @@ class Test_TestProgram(unittest.TestCase):
self.assertIn('\nUNEXPECTED SUCCESS: testUnexpectedSuccess ', out) self.assertIn('\nUNEXPECTED SUCCESS: testUnexpectedSuccess ', out)
expected = ('\n\nFAILED (failures=1, errors=1, skipped=1, ' expected = ('\n\nFAILED (failures=1, errors=1, skipped=1, '
'expected failures=1, unexpected successes=1)\n') 'expected failures=1, unexpected successes=1)\n')
self.assertTrue(out.endswith(expected)) self.assertEndsWith(out, expected)
@force_not_colorized @force_not_colorized
def test_ExitAsDefault(self): def test_ExitAsDefault(self):
@ -169,7 +169,7 @@ class Test_TestProgram(unittest.TestCase):
self.assertIn('\nUNEXPECTED SUCCESS: testUnexpectedSuccess ', out) self.assertIn('\nUNEXPECTED SUCCESS: testUnexpectedSuccess ', out)
expected = ('\n\nFAILED (failures=1, errors=1, skipped=1, ' expected = ('\n\nFAILED (failures=1, errors=1, skipped=1, '
'expected failures=1, unexpected successes=1)\n') 'expected failures=1, unexpected successes=1)\n')
self.assertTrue(out.endswith(expected)) self.assertEndsWith(out, expected)
@force_not_colorized @force_not_colorized
def test_ExitSkippedSuite(self): def test_ExitSkippedSuite(self):
@ -182,7 +182,7 @@ class Test_TestProgram(unittest.TestCase):
self.assertEqual(cm.exception.code, 0) self.assertEqual(cm.exception.code, 0)
out = stream.getvalue() out = stream.getvalue()
expected = '\n\nOK (skipped=1)\n' expected = '\n\nOK (skipped=1)\n'
self.assertTrue(out.endswith(expected)) self.assertEndsWith(out, expected)
@force_not_colorized @force_not_colorized
def test_ExitEmptySuite(self): def test_ExitEmptySuite(self):

View file

@ -462,7 +462,7 @@ class Test_TestResult(unittest.TestCase):
self.assertTrue(result.failfast) self.assertTrue(result.failfast)
result = runner.run(test) result = runner.run(test)
stream.flush() stream.flush()
self.assertTrue(stream.getvalue().endswith('\n\nOK\n')) self.assertEndsWith(stream.getvalue(), '\n\nOK\n')
class Test_TextTestResult(unittest.TestCase): class Test_TextTestResult(unittest.TestCase):

View file

@ -586,16 +586,16 @@ class AsyncMagicMethods(unittest.TestCase):
def test_magicmock_has_async_magic_methods(self): def test_magicmock_has_async_magic_methods(self):
m_mock = MagicMock() m_mock = MagicMock()
self.assertTrue(hasattr(m_mock, "__aenter__")) self.assertHasAttr(m_mock, "__aenter__")
self.assertTrue(hasattr(m_mock, "__aexit__")) self.assertHasAttr(m_mock, "__aexit__")
self.assertTrue(hasattr(m_mock, "__anext__")) self.assertHasAttr(m_mock, "__anext__")
def test_asyncmock_has_sync_magic_methods(self): def test_asyncmock_has_sync_magic_methods(self):
a_mock = AsyncMock() a_mock = AsyncMock()
self.assertTrue(hasattr(a_mock, "__enter__")) self.assertHasAttr(a_mock, "__enter__")
self.assertTrue(hasattr(a_mock, "__exit__")) self.assertHasAttr(a_mock, "__exit__")
self.assertTrue(hasattr(a_mock, "__next__")) self.assertHasAttr(a_mock, "__next__")
self.assertTrue(hasattr(a_mock, "__len__")) self.assertHasAttr(a_mock, "__len__")
def test_magic_methods_are_async_functions(self): def test_magic_methods_are_async_functions(self):
m_mock = MagicMock() m_mock = MagicMock()

View file

@ -23,21 +23,21 @@ class TestCallable(unittest.TestCase):
def test_non_callable(self): def test_non_callable(self):
for mock in NonCallableMagicMock(), NonCallableMock(): for mock in NonCallableMagicMock(), NonCallableMock():
self.assertRaises(TypeError, mock) self.assertRaises(TypeError, mock)
self.assertFalse(hasattr(mock, '__call__')) self.assertNotHasAttr(mock, '__call__')
self.assertIn(mock.__class__.__name__, repr(mock)) self.assertIn(mock.__class__.__name__, repr(mock))
def test_hierarchy(self): def test_hierarchy(self):
self.assertTrue(issubclass(MagicMock, Mock)) self.assertIsSubclass(MagicMock, Mock)
self.assertTrue(issubclass(NonCallableMagicMock, NonCallableMock)) self.assertIsSubclass(NonCallableMagicMock, NonCallableMock)
def test_attributes(self): def test_attributes(self):
one = NonCallableMock() one = NonCallableMock()
self.assertTrue(issubclass(type(one.one), Mock)) self.assertIsSubclass(type(one.one), Mock)
two = NonCallableMagicMock() two = NonCallableMagicMock()
self.assertTrue(issubclass(type(two.two), MagicMock)) self.assertIsSubclass(type(two.two), MagicMock)
def test_subclasses(self): def test_subclasses(self):
@ -45,13 +45,13 @@ class TestCallable(unittest.TestCase):
pass pass
one = MockSub() one = MockSub()
self.assertTrue(issubclass(type(one.one), MockSub)) self.assertIsSubclass(type(one.one), MockSub)
class MagicSub(MagicMock): class MagicSub(MagicMock):
pass pass
two = MagicSub() two = MagicSub()
self.assertTrue(issubclass(type(two.two), MagicSub)) self.assertIsSubclass(type(two.two), MagicSub)
def test_patch_spec(self): def test_patch_spec(self):

View file

@ -951,7 +951,7 @@ class SpecSignatureTest(unittest.TestCase):
proxy = Foo() proxy = Foo()
autospec = create_autospec(proxy) autospec = create_autospec(proxy)
self.assertFalse(hasattr(autospec, '__name__')) self.assertNotHasAttr(autospec, '__name__')
def test_autospec_signature_staticmethod(self): def test_autospec_signature_staticmethod(self):

View file

@ -10,13 +10,13 @@ class TestMockingMagicMethods(unittest.TestCase):
def test_deleting_magic_methods(self): def test_deleting_magic_methods(self):
mock = Mock() mock = Mock()
self.assertFalse(hasattr(mock, '__getitem__')) self.assertNotHasAttr(mock, '__getitem__')
mock.__getitem__ = Mock() mock.__getitem__ = Mock()
self.assertTrue(hasattr(mock, '__getitem__')) self.assertHasAttr(mock, '__getitem__')
del mock.__getitem__ del mock.__getitem__
self.assertFalse(hasattr(mock, '__getitem__')) self.assertNotHasAttr(mock, '__getitem__')
def test_magicmock_del(self): def test_magicmock_del(self):
@ -252,12 +252,12 @@ class TestMockingMagicMethods(unittest.TestCase):
self.assertEqual(list(mock), [1, 2, 3]) self.assertEqual(list(mock), [1, 2, 3])
getattr(mock, '__bool__').return_value = False getattr(mock, '__bool__').return_value = False
self.assertFalse(hasattr(mock, '__nonzero__')) self.assertNotHasAttr(mock, '__nonzero__')
self.assertFalse(bool(mock)) self.assertFalse(bool(mock))
for entry in _magics: for entry in _magics:
self.assertTrue(hasattr(mock, entry)) self.assertHasAttr(mock, entry)
self.assertFalse(hasattr(mock, '__imaginary__')) self.assertNotHasAttr(mock, '__imaginary__')
def test_magic_mock_equality(self): def test_magic_mock_equality(self):

View file

@ -2215,13 +2215,13 @@ class MockTest(unittest.TestCase):
def test_attribute_deletion(self): def test_attribute_deletion(self):
for mock in (Mock(), MagicMock(), NonCallableMagicMock(), for mock in (Mock(), MagicMock(), NonCallableMagicMock(),
NonCallableMock()): NonCallableMock()):
self.assertTrue(hasattr(mock, 'm')) self.assertHasAttr(mock, 'm')
del mock.m del mock.m
self.assertFalse(hasattr(mock, 'm')) self.assertNotHasAttr(mock, 'm')
del mock.f del mock.f
self.assertFalse(hasattr(mock, 'f')) self.assertNotHasAttr(mock, 'f')
self.assertRaises(AttributeError, getattr, mock, 'f') self.assertRaises(AttributeError, getattr, mock, 'f')
@ -2230,18 +2230,18 @@ class MockTest(unittest.TestCase):
for mock in (Mock(), MagicMock(), NonCallableMagicMock(), for mock in (Mock(), MagicMock(), NonCallableMagicMock(),
NonCallableMock()): NonCallableMock()):
mock.foo = 3 mock.foo = 3
self.assertTrue(hasattr(mock, 'foo')) self.assertHasAttr(mock, 'foo')
self.assertEqual(mock.foo, 3) self.assertEqual(mock.foo, 3)
del mock.foo del mock.foo
self.assertFalse(hasattr(mock, 'foo')) self.assertNotHasAttr(mock, 'foo')
mock.foo = 4 mock.foo = 4
self.assertTrue(hasattr(mock, 'foo')) self.assertHasAttr(mock, 'foo')
self.assertEqual(mock.foo, 4) self.assertEqual(mock.foo, 4)
del mock.foo del mock.foo
self.assertFalse(hasattr(mock, 'foo')) self.assertNotHasAttr(mock, 'foo')
def test_mock_raises_when_deleting_nonexistent_attribute(self): def test_mock_raises_when_deleting_nonexistent_attribute(self):
@ -2259,7 +2259,7 @@ class MockTest(unittest.TestCase):
mock.child = True mock.child = True
del mock.child del mock.child
mock.reset_mock() mock.reset_mock()
self.assertFalse(hasattr(mock, 'child')) self.assertNotHasAttr(mock, 'child')
def test_class_assignable(self): def test_class_assignable(self):

View file

@ -366,7 +366,7 @@ class PatchTest(unittest.TestCase):
self.assertEqual(SomeClass.frooble, sentinel.Frooble) self.assertEqual(SomeClass.frooble, sentinel.Frooble)
test() test()
self.assertFalse(hasattr(SomeClass, 'frooble')) self.assertNotHasAttr(SomeClass, 'frooble')
def test_patch_wont_create_by_default(self): def test_patch_wont_create_by_default(self):
@ -383,7 +383,7 @@ class PatchTest(unittest.TestCase):
@patch.object(SomeClass, 'ord', sentinel.Frooble) @patch.object(SomeClass, 'ord', sentinel.Frooble)
def test(): pass def test(): pass
test() test()
self.assertFalse(hasattr(SomeClass, 'ord')) self.assertNotHasAttr(SomeClass, 'ord')
def test_patch_builtins_without_create(self): def test_patch_builtins_without_create(self):
@ -1477,7 +1477,7 @@ class PatchTest(unittest.TestCase):
finally: finally:
patcher.stop() patcher.stop()
self.assertFalse(hasattr(Foo, 'blam')) self.assertNotHasAttr(Foo, 'blam')
def test_patch_multiple_spec_set(self): def test_patch_multiple_spec_set(self):

View file

@ -111,10 +111,6 @@ class BaseTest(unittest.TestCase):
result = f.read() result = f.read()
return result return result
def assertEndsWith(self, string, tail):
if not string.endswith(tail):
self.fail(f"String {string!r} does not end with {tail!r}")
class BasicTest(BaseTest): class BasicTest(BaseTest):
"""Test venv module functionality.""" """Test venv module functionality."""

View file

@ -1321,13 +1321,67 @@ class TestCase(object):
"""Same as self.assertTrue(isinstance(obj, cls)), with a nicer """Same as self.assertTrue(isinstance(obj, cls)), with a nicer
default message.""" default message."""
if not isinstance(obj, cls): if not isinstance(obj, cls):
standardMsg = '%s is not an instance of %r' % (safe_repr(obj), cls) if isinstance(cls, tuple):
standardMsg = f'{safe_repr(obj)} is not an instance of any of {cls!r}'
else:
standardMsg = f'{safe_repr(obj)} is not an instance of {cls!r}'
self.fail(self._formatMessage(msg, standardMsg)) self.fail(self._formatMessage(msg, standardMsg))
def assertNotIsInstance(self, obj, cls, msg=None): def assertNotIsInstance(self, obj, cls, msg=None):
"""Included for symmetry with assertIsInstance.""" """Included for symmetry with assertIsInstance."""
if isinstance(obj, cls): if isinstance(obj, cls):
standardMsg = '%s is an instance of %r' % (safe_repr(obj), cls) if isinstance(cls, tuple):
for x in cls:
if isinstance(obj, x):
cls = x
break
standardMsg = f'{safe_repr(obj)} is an instance of {cls!r}'
self.fail(self._formatMessage(msg, standardMsg))
def assertIsSubclass(self, cls, superclass, msg=None):
try:
if issubclass(cls, superclass):
return
except TypeError:
if not isinstance(cls, type):
self.fail(self._formatMessage(msg, f'{cls!r} is not a class'))
raise
if isinstance(superclass, tuple):
standardMsg = f'{cls!r} is not a subclass of any of {superclass!r}'
else:
standardMsg = f'{cls!r} is not a subclass of {superclass!r}'
self.fail(self._formatMessage(msg, standardMsg))
def assertNotIsSubclass(self, cls, superclass, msg=None):
try:
if not issubclass(cls, superclass):
return
except TypeError:
if not isinstance(cls, type):
self.fail(self._formatMessage(msg, f'{cls!r} is not a class'))
raise
if isinstance(superclass, tuple):
for x in superclass:
if issubclass(cls, x):
superclass = x
break
standardMsg = f'{cls!r} is a subclass of {superclass!r}'
self.fail(self._formatMessage(msg, standardMsg))
def assertHasAttr(self, obj, name, msg=None):
if not hasattr(obj, name):
if isinstance(obj, types.ModuleType):
standardMsg = f'module {obj.__name__!r} has no attribute {name!r}'
else:
standardMsg = f'{type(obj).__name__} instance has no attribute {name!r}'
self.fail(self._formatMessage(msg, standardMsg))
def assertNotHasAttr(self, obj, name, msg=None):
if hasattr(obj, name):
if isinstance(obj, types.ModuleType):
standardMsg = f'module {obj.__name__!r} has unexpected attribute {name!r}'
else:
standardMsg = f'{type(obj).__name__} instance has unexpected attribute {name!r}'
self.fail(self._formatMessage(msg, standardMsg)) self.fail(self._formatMessage(msg, standardMsg))
def assertRaisesRegex(self, expected_exception, expected_regex, def assertRaisesRegex(self, expected_exception, expected_regex,
@ -1391,6 +1445,80 @@ class TestCase(object):
msg = self._formatMessage(msg, standardMsg) msg = self._formatMessage(msg, standardMsg)
raise self.failureException(msg) raise self.failureException(msg)
def _tail_type_check(self, s, tails, msg):
if not isinstance(tails, tuple):
tails = (tails,)
for tail in tails:
if isinstance(tail, str):
if not isinstance(s, str):
self.fail(self._formatMessage(msg,
f'Expected str, not {type(s).__name__}'))
elif isinstance(tail, (bytes, bytearray)):
if not isinstance(s, (bytes, bytearray)):
self.fail(self._formatMessage(msg,
f'Expected bytes, not {type(s).__name__}'))
def assertStartsWith(self, s, prefix, msg=None):
try:
if s.startswith(prefix):
return
except (AttributeError, TypeError):
self._tail_type_check(s, prefix, msg)
raise
a = safe_repr(s, short=True)
b = safe_repr(prefix)
if isinstance(prefix, tuple):
standardMsg = f"{a} doesn't start with any of {b}"
else:
standardMsg = f"{a} doesn't start with {b}"
self.fail(self._formatMessage(msg, standardMsg))
def assertNotStartsWith(self, s, prefix, msg=None):
try:
if not s.startswith(prefix):
return
except (AttributeError, TypeError):
self._tail_type_check(s, prefix, msg)
raise
if isinstance(prefix, tuple):
for x in prefix:
if s.startswith(x):
prefix = x
break
a = safe_repr(s, short=True)
b = safe_repr(prefix)
self.fail(self._formatMessage(msg, f"{a} starts with {b}"))
def assertEndsWith(self, s, suffix, msg=None):
try:
if s.endswith(suffix):
return
except (AttributeError, TypeError):
self._tail_type_check(s, suffix, msg)
raise
a = safe_repr(s, short=True)
b = safe_repr(suffix)
if isinstance(suffix, tuple):
standardMsg = f"{a} doesn't end with any of {b}"
else:
standardMsg = f"{a} doesn't end with {b}"
self.fail(self._formatMessage(msg, standardMsg))
def assertNotEndsWith(self, s, suffix, msg=None):
try:
if not s.endswith(suffix):
return
except (AttributeError, TypeError):
self._tail_type_check(s, suffix, msg)
raise
if isinstance(suffix, tuple):
for x in suffix:
if s.endswith(x):
suffix = x
break
a = safe_repr(s, short=True)
b = safe_repr(suffix)
self.fail(self._formatMessage(msg, f"{a} ends with {b}"))
class FunctionTestCase(TestCase): class FunctionTestCase(TestCase):

View file

@ -0,0 +1,9 @@
Add new assertion methods for :mod:`unittest`:
:meth:`~unittest.TestCase.assertHasAttr`,
:meth:`~unittest.TestCase.assertNotHasAttr`,
:meth:`~unittest.TestCase.assertIsSubclass`,
:meth:`~unittest.TestCase.assertNotIsSubclass`
:meth:`~unittest.TestCase.assertStartsWith`,
:meth:`~unittest.TestCase.assertNotStartsWith`,
:meth:`~unittest.TestCase.assertEndsWith` and
:meth:`~unittest.TestCase.assertNotEndsWith`.