Skip to content

gh-71339: Add additional assertion methods for unittest #128707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Doc/library/unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,12 @@ Test cases
| :meth:`assertNotIsInstance(a, b) | ``not isinstance(a, b)`` | 3.2 |
| <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
as the error message on failure (see also :data:`longMessage`).
Expand Down Expand Up @@ -960,6 +966,14 @@ Test cases

.. 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
Expand Down Expand Up @@ -1210,6 +1224,24 @@ Test cases
| <TestCase.assertCountEqual>` | elements in the same number, | |
| | 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)
Expand Down Expand Up @@ -1278,6 +1310,31 @@ Test cases

.. 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:

Expand Down
17 changes: 17 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,23 @@ unittest
directory again. It was removed in Python 3.11.
(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
------
Expand Down
8 changes: 0 additions & 8 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,14 +405,6 @@ def test_wrap_lenfunc_bad_cast(self):

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):
# Testing Python subclass of dict...
self.assertTrue(issubclass(dict, dict))
Expand Down
6 changes: 3 additions & 3 deletions Lib/test/test_gdb/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def gdb_has_frame_select():
"Python was compiled with optimizations")
class PyListTests(DebuggerTests):
def assertListing(self, expected, actual):
self.assertEndsWith(actual, expected)
self.assertEndswith(actual, expected)

def test_basic_command(self):
'Verify that the "py-list" command works'
Expand Down Expand Up @@ -103,15 +103,15 @@ def test_down_at_bottom(self):
'Verify handling of "py-down" at the bottom of the stack'
bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-down'])
self.assertEndsWith(bt,
self.assertEndswith(bt,
'Unable to find a newer python frame\n')

@unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands")
def test_up_at_top(self):
'Verify handling of "py-up" at the top of the stack'
bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-up'] * 5)
self.assertEndsWith(bt,
self.assertEndswith(bt,
'Unable to find an older python frame\n')

@unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands")
Expand Down
5 changes: 0 additions & 5 deletions Lib/test/test_gdb/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,6 @@ def get_stack_trace(self, source=None, script=None,

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):
m = re.match(pattern, actual, re.DOTALL)
if not m:
Expand Down
10 changes: 2 additions & 8 deletions Lib/test/test_importlib/resources/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,6 @@ def _gen_resourcetxt_path_parts(self):
with self.subTest(path_parts=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):
self.assertEqual(
resources.read_text(self.anchor01, 'utf-8.file'),
Expand Down Expand Up @@ -89,7 +83,7 @@ def test_read_text(self):
),
'\x00\x01\x02\x03',
)
self.assertEndsWith( # ignore the BOM
self.assertEndswith( # ignore the BOM
resources.read_text(
self.anchor01,
'utf-16.file',
Expand Down Expand Up @@ -141,7 +135,7 @@ def test_open_text(self):
'utf-16.file',
errors='backslashreplace',
) as f:
self.assertEndsWith( # ignore the BOM
self.assertEndswith( # ignore the BOM
f.read(),
'Hello, UTF-16 world!\n'.encode('utf-16-le').decode(
errors='backslashreplace',
Expand Down
10 changes: 1 addition & 9 deletions Lib/test/test_pyclbr.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@ def assertListEq(self, l1, l2, ignore):
print("l1=%r\nl2=%r\nignore=%r" % (l1, l2, ignore), file=sys.stderr)
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):
''' succeed iff key in obj or key in ignore. '''
if key in ignore: return
Expand Down Expand Up @@ -86,7 +78,7 @@ def ismethod(oclass, obj, name):
for name, value in dict.items():
if name in ignore:
continue
self.assertHasattr(module, name, ignore)
self.assertHasAttr(module, name, ignore)
py_item = getattr(module, name)
if isinstance(value, pyclbr.Function):
self.assertIsInstance(py_item, (FunctionType, BuiltinFunctionType))
Expand Down
Loading
Loading