Skip to content

Commit 06cad77

Browse files
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().
1 parent 41f7350 commit 06cad77

20 files changed

+555
-100
lines changed

Doc/library/unittest.rst

+61
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,12 @@ Test cases
883883
| :meth:`assertNotIsInstance(a, b) | ``not isinstance(a, b)`` | 3.2 |
884884
| <TestCase.assertNotIsInstance>` | | |
885885
+-----------------------------------------+-----------------------------+---------------+
886+
| :meth:`assertIsSubclass(a, b) | ``issubclass(a, b)`` | 3.14 |
887+
| <TestCase.assertIsSubclass>` | | |
888+
+-----------------------------------------+-----------------------------+---------------+
889+
| :meth:`assertNotIsSubclass(a, b) | ``not issubclass(a, b)`` | 3.14 |
890+
| <TestCase.assertNotIsSubclass>` | | |
891+
+-----------------------------------------+-----------------------------+---------------+
886892

887893
All the assert methods accept a *msg* argument that, if specified, is used
888894
as the error message on failure (see also :data:`longMessage`).
@@ -961,6 +967,15 @@ Test cases
961967
.. versionadded:: 3.2
962968

963969

970+
.. method:: assertIsSubclass(cls, superclass, msg=None)
971+
assertNotIsSubclass(cls, superclass, msg=None)
972+
973+
Test that *cls* is (or is not) a subclass of *superclass* (which can be a
974+
class or a tuple of classes, as supported by :func:`issubclass`).
975+
To check for the exact type, use :func:`assertIs(cls, superclass) <assertIs>`.
976+
977+
.. versionadded:: next
978+
964979

965980
It is also possible to check the production of exceptions, warnings, and
966981
log messages using the following methods:
@@ -1210,6 +1225,24 @@ Test cases
12101225
| <TestCase.assertCountEqual>` | elements in the same number, | |
12111226
| | regardless of their order. | |
12121227
+---------------------------------------+--------------------------------+--------------+
1228+
| :meth:`assertStartsWith(a, b) | ``a.startswith(b)`` | 3.14 |
1229+
| <TestCase.assertStartsWith>` | | |
1230+
+---------------------------------------+--------------------------------+--------------+
1231+
| :meth:`assertNotStartsWith(a, b) | ``not a.startswith(b)`` | 3.14 |
1232+
| <TestCase.assertNotStartsWith>` | | |
1233+
+---------------------------------------+--------------------------------+--------------+
1234+
| :meth:`assertEndsWith(a, b) | ``a.endswith(b)`` | 3.14 |
1235+
| <TestCase.assertEndsWith>` | | |
1236+
+---------------------------------------+--------------------------------+--------------+
1237+
| :meth:`assertNotEndsWith(a, b) | ``not a.endswith(b)`` | 3.14 |
1238+
| <TestCase.assertNotEndsWith>` | | |
1239+
+---------------------------------------+--------------------------------+--------------+
1240+
| :meth:`assertHasAttr(a, b) | ``hastattr(a, b)`` | 3.14 |
1241+
| <TestCase.assertHasAttr>` | | |
1242+
+---------------------------------------+--------------------------------+--------------+
1243+
| :meth:`assertNotHasAttr(a, b) | ``not hastattr(a, b)`` | 3.14 |
1244+
| <TestCase.assertNotHasAttr>` | | |
1245+
+---------------------------------------+--------------------------------+--------------+
12131246

12141247

12151248
.. method:: assertAlmostEqual(first, second, places=7, msg=None, delta=None)
@@ -1279,6 +1312,34 @@ Test cases
12791312
.. versionadded:: 3.2
12801313

12811314

1315+
.. method:: assertStartsWith(s, prefix, msg=None)
1316+
.. method:: assertNotStartsWith(s, prefix, msg=None)
1317+
1318+
Test that the Unicode or byte string *s* starts (or does not start)
1319+
with a *prefix*.
1320+
*prefix* can also be a tuple of strings to try.
1321+
1322+
.. versionadded:: next
1323+
1324+
1325+
.. method:: assertEndsWith(s, suffix, msg=None)
1326+
.. method:: assertNotEndsWith(s, suffix, msg=None)
1327+
1328+
Test that the Unicode or byte string *s* ends (or does not end)
1329+
with a *suffix*.
1330+
*suffix* can also be a tuple of strings to try.
1331+
1332+
.. versionadded:: next
1333+
1334+
1335+
.. method:: assertHasAttr(obj, name, msg=None)
1336+
.. method:: assertNotHasAttr(obj, name, msg=None)
1337+
1338+
Test that the object *obj* has (or has not) an attribute *name*.
1339+
1340+
.. versionadded:: next
1341+
1342+
12821343
.. _type-specific-methods:
12831344

12841345
The :meth:`assertEqual` method dispatches the equality check for objects of

Doc/whatsnew/3.14.rst

+17
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,23 @@ unittest
670670
directory again. It was removed in Python 3.11.
671671
(Contributed by Jacob Walls in :gh:`80958`.)
672672

673+
* A number of new methods were added in the :class:`~unittest.TestCase` class
674+
that provide more specialized tests.
675+
676+
- :meth:`~unittest.TestCase.assertHasAttr` and
677+
:meth:`~unittest.TestCase.assertNotHasAttr` check whether the object
678+
has a particular attribute.
679+
- :meth:`~unittest.TestCase.assertIsSubclass` and
680+
:meth:`~unittest.TestCase.assertNotIsSubclass` check whether the object
681+
is a subclass of a particular class, or of one of a tuple of classes.
682+
- :meth:`~unittest.TestCase.assertStartsWith`,
683+
:meth:`~unittest.TestCase.assertNotStartsWith`,
684+
:meth:`~unittest.TestCase.assertEndsWith` and
685+
:meth:`~unittest.TestCase.assertNotEndsWith` check whether the Unicode
686+
or byte string starts or ends with particular string(s).
687+
688+
(Contributed by Serhiy Storchaka in :gh:`71339`.)
689+
673690

674691
urllib
675692
------

Lib/test/test_descr.py

-8
Original file line numberDiff line numberDiff line change
@@ -405,14 +405,6 @@ def test_wrap_lenfunc_bad_cast(self):
405405

406406
class ClassPropertiesAndMethods(unittest.TestCase):
407407

408-
def assertHasAttr(self, obj, name):
409-
self.assertTrue(hasattr(obj, name),
410-
'%r has no attribute %r' % (obj, name))
411-
412-
def assertNotHasAttr(self, obj, name):
413-
self.assertFalse(hasattr(obj, name),
414-
'%r has unexpected attribute %r' % (obj, name))
415-
416408
def test_python_dicts(self):
417409
# Testing Python subclass of dict...
418410
self.assertTrue(issubclass(dict, dict))

Lib/test/test_gdb/util.py

-5
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,6 @@ def get_stack_trace(self, source=None, script=None,
280280

281281
return out
282282

283-
def assertEndsWith(self, actual, exp_end):
284-
'''Ensure that the given "actual" string ends with "exp_end"'''
285-
self.assertTrue(actual.endswith(exp_end),
286-
msg='%r did not end with %r' % (actual, exp_end))
287-
288283
def assertMultilineMatches(self, actual, pattern):
289284
m = re.match(pattern, actual, re.DOTALL)
290285
if not m:

Lib/test/test_importlib/resources/test_functional.py

-6
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,6 @@ def _gen_resourcetxt_path_parts(self):
4343
with self.subTest(path_parts=path_parts):
4444
yield path_parts
4545

46-
def assertEndsWith(self, string, suffix):
47-
"""Assert that `string` ends with `suffix`.
48-
49-
Used to ignore an architecture-specific UTF-16 byte-order mark."""
50-
self.assertEqual(string[-len(suffix) :], suffix)
51-
5246
def test_read_text(self):
5347
self.assertEqual(
5448
resources.read_text(self.anchor01, 'utf-8.file'),

Lib/test/test_pyclbr.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,6 @@ def assertListEq(self, l1, l2, ignore):
3131
print("l1=%r\nl2=%r\nignore=%r" % (l1, l2, ignore), file=sys.stderr)
3232
self.fail("%r missing" % missing.pop())
3333

34-
def assertHasattr(self, obj, attr, ignore):
35-
''' succeed iff hasattr(obj,attr) or attr in ignore. '''
36-
if attr in ignore: return
37-
if not hasattr(obj, attr): print("???", attr)
38-
self.assertTrue(hasattr(obj, attr),
39-
'expected hasattr(%r, %r)' % (obj, attr))
40-
41-
4234
def assertHaskey(self, obj, key, ignore):
4335
''' succeed iff key in obj or key in ignore. '''
4436
if key in ignore: return
@@ -86,7 +78,7 @@ def ismethod(oclass, obj, name):
8678
for name, value in dict.items():
8779
if name in ignore:
8880
continue
89-
self.assertHasattr(module, name, ignore)
81+
self.assertHasAttr(module, name, ignore)
9082
py_item = getattr(module, name)
9183
if isinstance(value, pyclbr.Function):
9284
self.assertIsInstance(py_item, (FunctionType, BuiltinFunctionType))

Lib/test/test_typing.py

-18
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,6 @@
5959

6060
class BaseTestCase(TestCase):
6161

62-
def assertIsSubclass(self, cls, class_or_tuple, msg=None):
63-
if not issubclass(cls, class_or_tuple):
64-
message = '%r is not a subclass of %r' % (cls, class_or_tuple)
65-
if msg is not None:
66-
message += ' : %s' % msg
67-
raise self.failureException(message)
68-
69-
def assertNotIsSubclass(self, cls, class_or_tuple, msg=None):
70-
if issubclass(cls, class_or_tuple):
71-
message = '%r is a subclass of %r' % (cls, class_or_tuple)
72-
if msg is not None:
73-
message += ' : %s' % msg
74-
raise self.failureException(message)
75-
7662
def clear_caches(self):
7763
for f in typing._cleanups:
7864
f()
@@ -1252,10 +1238,6 @@ class Gen[*Ts]: ...
12521238

12531239
class TypeVarTupleTests(BaseTestCase):
12541240

1255-
def assertEndsWith(self, string, tail):
1256-
if not string.endswith(tail):
1257-
self.fail(f"String {string!r} does not end with {tail!r}")
1258-
12591241
def test_name(self):
12601242
Ts = TypeVarTuple('Ts')
12611243
self.assertEqual(Ts.__name__, 'Ts')

0 commit comments

Comments
 (0)