Skip to content

Commit

Permalink
gh-71339: Add additional assertion methods for unittest (GH-128707)
Browse files Browse the repository at this point in the history
Add the following methods:

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

Also improve error messages for assertIsInstance() and
assertNotIsInstance().

Backports: 06cad77a5b345adde88609be9c3c470c5cd9f417
Signed-off-by: Chris Withers <chris@withers.org>
  • Loading branch information
serhiy-storchaka authored and cjw296 committed Mar 3, 2025
1 parent 9e3f49c commit 45d2d49
Show file tree
Hide file tree
Showing 7 changed files with 41 additions and 32 deletions.
9 changes: 9 additions & 0 deletions NEWS.d/2025-01-10-15-06-45.gh-issue-71339.EKnpzw.rst
Original file line number Diff line number Diff line change
@@ -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`.
14 changes: 7 additions & 7 deletions mock/tests/testasync.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,16 +600,16 @@ def test_sync_magic_methods_return_magic_mocks(self):

def test_magicmock_has_async_magic_methods(self):
m_mock = MagicMock()
self.assertTrue(hasattr(m_mock, "__aenter__"))
self.assertTrue(hasattr(m_mock, "__aexit__"))
self.assertTrue(hasattr(m_mock, "__anext__"))
self.assertHasAttr(m_mock, "__aenter__")
self.assertHasAttr(m_mock, "__aexit__")
self.assertHasAttr(m_mock, "__anext__")

def test_asyncmock_has_sync_magic_methods(self):
a_mock = AsyncMock()
self.assertTrue(hasattr(a_mock, "__enter__"))
self.assertTrue(hasattr(a_mock, "__exit__"))
self.assertTrue(hasattr(a_mock, "__next__"))
self.assertTrue(hasattr(a_mock, "__len__"))
self.assertHasAttr(a_mock, "__enter__")
self.assertHasAttr(a_mock, "__exit__")
self.assertHasAttr(a_mock, "__next__")
self.assertHasAttr(a_mock, "__len__")

def test_magic_methods_are_async_functions(self):
m_mock = MagicMock()
Expand Down
14 changes: 7 additions & 7 deletions mock/tests/testcallable.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,35 +22,35 @@ def assertNotCallable(self, mock):
def test_non_callable(self):
for mock in NonCallableMagicMock(), NonCallableMock():
self.assertRaises(TypeError, mock)
self.assertFalse(hasattr(mock, '__call__'))
self.assertNotHasAttr(mock, '__call__')
self.assertIn(mock.__class__.__name__, repr(mock))


def test_hierarchy(self):
self.assertTrue(issubclass(MagicMock, Mock))
self.assertTrue(issubclass(NonCallableMagicMock, NonCallableMock))
self.assertIsSubclass(MagicMock, Mock)
self.assertIsSubclass(NonCallableMagicMock, NonCallableMock)


def test_attributes(self):
one = NonCallableMock()
self.assertTrue(issubclass(type(one.one), Mock))
self.assertIsSubclass(type(one.one), Mock)

two = NonCallableMagicMock()
self.assertTrue(issubclass(type(two.two), MagicMock))
self.assertIsSubclass(type(two.two), MagicMock)


def test_subclasses(self):
class MockSub(Mock):
pass

one = MockSub()
self.assertTrue(issubclass(type(one.one), MockSub))
self.assertIsSubclass(type(one.one), MockSub)

class MagicSub(MagicMock):
pass

two = MagicSub()
self.assertTrue(issubclass(type(two.two), MagicSub))
self.assertIsSubclass(type(two.two), MagicSub)


def test_patch_spec(self):
Expand Down
2 changes: 1 addition & 1 deletion mock/tests/testhelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,7 @@ def __getattr__(self, attribute):

proxy = Foo()
autospec = create_autospec(proxy)
self.assertFalse(hasattr(autospec, '__name__'))
self.assertNotHasAttr(autospec, '__name__')


def test_autospec_signature_staticmethod(self):
Expand Down
12 changes: 6 additions & 6 deletions mock/tests/testmagicmethods.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ class TestMockingMagicMethods(unittest.TestCase):

def test_deleting_magic_methods(self):
mock = Mock()
self.assertFalse(hasattr(mock, '__getitem__'))
self.assertNotHasAttr(mock, '__getitem__')

mock.__getitem__ = Mock()
self.assertTrue(hasattr(mock, '__getitem__'))
self.assertHasAttr(mock, '__getitem__')

del mock.__getitem__
self.assertFalse(hasattr(mock, '__getitem__'))
self.assertNotHasAttr(mock, '__getitem__')


def test_magicmock_del(self):
Expand Down Expand Up @@ -252,12 +252,12 @@ def test_magicmock(self):
self.assertEqual(list(mock), [1, 2, 3])

getattr(mock, '__bool__').return_value = False
self.assertFalse(hasattr(mock, '__nonzero__'))
self.assertNotHasAttr(mock, '__nonzero__')
self.assertFalse(bool(mock))

for entry in _magics:
self.assertTrue(hasattr(mock, entry))
self.assertFalse(hasattr(mock, '__imaginary__'))
self.assertHasAttr(mock, entry)
self.assertNotHasAttr(mock, '__imaginary__')


def test_magic_mock_equality(self):
Expand Down
16 changes: 8 additions & 8 deletions mock/tests/testmock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2225,13 +2225,13 @@ def test_attach_mock_patch_autospec_signature(self):
def test_attribute_deletion(self):
for mock in (Mock(), MagicMock(), NonCallableMagicMock(),
NonCallableMock()):
self.assertTrue(hasattr(mock, 'm'))
self.assertHasAttr(mock, 'm')

del mock.m
self.assertFalse(hasattr(mock, 'm'))
self.assertNotHasAttr(mock, 'm')

del mock.f
self.assertFalse(hasattr(mock, 'f'))
self.assertNotHasAttr(mock, 'f')
self.assertRaises(AttributeError, getattr, mock, 'f')


Expand All @@ -2240,18 +2240,18 @@ def test_mock_does_not_raise_on_repeated_attribute_deletion(self):
for mock in (Mock(), MagicMock(), NonCallableMagicMock(),
NonCallableMock()):
mock.foo = 3
self.assertTrue(hasattr(mock, 'foo'))
self.assertHasAttr(mock, 'foo')
self.assertEqual(mock.foo, 3)

del mock.foo
self.assertFalse(hasattr(mock, 'foo'))
self.assertNotHasAttr(mock, 'foo')

mock.foo = 4
self.assertTrue(hasattr(mock, 'foo'))
self.assertHasAttr(mock, 'foo')
self.assertEqual(mock.foo, 4)

del mock.foo
self.assertFalse(hasattr(mock, 'foo'))
self.assertNotHasAttr(mock, 'foo')


def test_mock_raises_when_deleting_nonexistent_attribute(self):
Expand All @@ -2269,7 +2269,7 @@ def test_reset_mock_does_not_raise_on_attr_deletion(self):
mock.child = True
del mock.child
mock.reset_mock()
self.assertFalse(hasattr(mock, 'child'))
self.assertNotHasAttr(mock, 'child')


def test_class_assignable(self):
Expand Down
6 changes: 3 additions & 3 deletions mock/tests/testpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ def test():
self.assertEqual(SomeClass.frooble, sentinel.Frooble)

test()
self.assertFalse(hasattr(SomeClass, 'frooble'))
self.assertNotHasAttr(SomeClass, 'frooble')


def test_patch_wont_create_by_default(self):
Expand All @@ -381,7 +381,7 @@ def test_patchobject_wont_create_by_default(self):
@patch.object(SomeClass, 'ord', sentinel.Frooble)
def test(): pass
test()
self.assertFalse(hasattr(SomeClass, 'ord'))
self.assertNotHasAttr(SomeClass, 'ord')


def test_patch_builtins_without_create(self):
Expand Down Expand Up @@ -1475,7 +1475,7 @@ def test_patch_multiple_create(self):
finally:
patcher.stop()

self.assertFalse(hasattr(Foo, 'blam'))
self.assertNotHasAttr(Foo, 'blam')


def test_patch_multiple_spec_set(self):
Expand Down

0 comments on commit 45d2d49

Please sign in to comment.