diff options
| author | 2019-10-14 22:13:22 +0200 | |
|---|---|---|
| committer | 2019-10-14 22:13:22 +0200 | |
| commit | e4e01cd5388da19435637353e711c2feab5a0e59 (patch) | |
| tree | 7284fdb417e8b1e42ea55a9d8d48c20f933dc9c3 | |
| parent | Remove empty tests.cogs folder (diff) | |
Add more specialized Mocks to tests.helpers
This commit introduces some new Mock-types to the already existing
Mock-types for discord.py objects. The total list is now:
- MockGuild
- MockRole
- MockMember
- MockBot
- MockContext
- MockTextChannel
- MockMessage
In addition, I've added all coroutines in the documentation for these
discord.py objects as `AsyncMock` attributes to ease testing. Tests
ensure that the attributes set for the Mocks exist for the actual
discord.py objects as well.
| -rw-r--r-- | tests/helpers.py | 179 | ||||
| -rw-r--r-- | tests/test_helpers.py | 385 | 
2 files changed, 383 insertions, 181 deletions
| diff --git a/tests/helpers.py b/tests/helpers.py index 18c9866bf..f8e8357f1 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -143,6 +143,32 @@ class MockGuild(AttributeMock, unittest.mock.Mock, HashableMixin):          if members:              self.members.extend(members) +        # `discord.Guild` coroutines +        self.create_category_channel = AsyncMock() +        self.ban = AsyncMock() +        self.bans = AsyncMock() +        self.create_category = AsyncMock() +        self.create_custom_emoji = AsyncMock() +        self.create_role = AsyncMock() +        self.create_text_channel = AsyncMock() +        self.create_voice_channel = AsyncMock() +        self.delete = AsyncMock() +        self.edit = AsyncMock() +        self.estimate_pruned_members = AsyncMock() +        self.fetch_ban = AsyncMock() +        self.fetch_channels = AsyncMock() +        self.fetch_emoji = AsyncMock() +        self.fetch_emojis = AsyncMock() +        self.fetch_member = AsyncMock() +        self.invites = AsyncMock() +        self.kick = AsyncMock() +        self.leave = AsyncMock() +        self.prune_members = AsyncMock() +        self.unban = AsyncMock() +        self.vanity_invite = AsyncMock() +        self.webhooks = AsyncMock() +        self.widget = AsyncMock() +  # Create a Role instance to get a realistic Mock of `discord.Role`  role_data = {'name': 'role', 'id': 1} @@ -167,6 +193,10 @@ class MockRole(AttributeMock, unittest.mock.Mock, ColourMixin, HashableMixin):          self.position = position          self.mention = f'&{self.name}' +        # 'discord.Role' coroutines +        self.delete = AsyncMock() +        self.edit = AsyncMock() +      def __lt__(self, other):          """Simplified position-based comparisons similar to those of `discord.Role`."""          return self.position < other.position @@ -205,7 +235,19 @@ class MockMember(AttributeMock, unittest.mock.Mock, ColourMixin, HashableMixin):              self.roles.extend(roles)          self.mention = f"@{self.name}" + +        # `discord.Member` coroutines +        self.add_roles = AsyncMock() +        self.ban = AsyncMock() +        self.edit = AsyncMock() +        self.fetch_message = AsyncMock() +        self.kick = AsyncMock() +        self.move_to = AsyncMock() +        self.pins = AsyncMock() +        self.remove_roles = AsyncMock()          self.send = AsyncMock() +        self.trigger_typing = AsyncMock() +        self.unban = AsyncMock()  # Create a Bot instance to get a realistic MagicMock of `discord.ext.commands.Bot` @@ -224,9 +266,37 @@ class MockBot(AttributeMock, unittest.mock.MagicMock):      def __init__(self, **kwargs) -> None:          super().__init__(spec=bot_instance, **kwargs) + +        # `discord.ext.commands.Bot` coroutines          self._before_invoke = AsyncMock()          self._after_invoke = AsyncMock() -        self.user = MockMember(name="Python", user_id=123456789) +        self.application_info = AsyncMock() +        self.change_presence = AsyncMock() +        self.connect = AsyncMock() +        self.close = AsyncMock() +        self.create_guild = AsyncMock() +        self.delete_invite = AsyncMock() +        self.fetch_channel = AsyncMock() +        self.fetch_guild = AsyncMock() +        self.fetch_guilds = AsyncMock() +        self.fetch_invite = AsyncMock() +        self.fetch_user = AsyncMock() +        self.fetch_user_profile = AsyncMock() +        self.fetch_webhook = AsyncMock() +        self.fetch_widget = AsyncMock() +        self.get_context = AsyncMock() +        self.get_prefix = AsyncMock() +        self.invoke = AsyncMock() +        self.is_owner = AsyncMock() +        self.login = AsyncMock() +        self.logout = AsyncMock() +        self.on_command_error = AsyncMock() +        self.on_error = AsyncMock() +        self.process_commands = AsyncMock() +        self.request_offline_members = AsyncMock() +        self.start = AsyncMock() +        self.wait_until_ready = AsyncMock() +        self.wait_for = AsyncMock()  # Create a Context instance to get a realistic MagicMock of `discord.ext.commands.Context` @@ -246,7 +316,112 @@ class MockContext(AttributeMock, unittest.mock.MagicMock):      def __init__(self, **kwargs) -> None:          super().__init__(spec=context_instance, **kwargs)          self.bot = MockBot() -        self.send = AsyncMock()          self.guild = MockGuild()          self.author = MockMember()          self.command = unittest.mock.MagicMock() + +        # `discord.ext.commands.Context` coroutines +        self.fetch_message = AsyncMock() +        self.invoke = AsyncMock() +        self.pins = AsyncMock() +        self.reinvoke = AsyncMock() +        self.send = AsyncMock() +        self.send_help = AsyncMock() +        self.trigger_typing = AsyncMock() + + +# Create a TextChannel instance to get a realistic MagicMock of `discord.TextChannel` +channel_data = { +    'id': 1, +    'type': 'TextChannel', +    'name': 'channel', +    'parent_id': 1234567890, +    'topic': 'topic', +    'position': 1, +    'nsfw': False, +    'last_message_id': 1, +} +state = unittest.mock.MagicMock() +guild = unittest.mock.MagicMock() +channel_instance = discord.TextChannel(state=state, guild=guild, data=channel_data) + + +class MockTextChannel(AttributeMock, unittest.mock.Mock, HashableMixin): +    """ +    A MagicMock subclass to mock TextChannel objects. + +    Instances of this class will follow the specifications of `discord.TextChannel` instances. For +    more information, see the `MockGuild` docstring. +    """ + +    attribute_mocktype = unittest.mock.MagicMock + +    def __init__(self, name: str = 'channel', channel_id: int = 1, **kwargs) -> None: +        super().__init__(spec=channel_instance, **kwargs) +        self.id = channel_id +        self.name = name +        self.guild = MockGuild() +        self.mention = f"#{self.name}" + +        # `discord.TextChannel` coroutines +        self.clone = AsyncMock() +        self.create_invite = AsyncMock() +        self.create_webhook = AsyncMock() +        self.delete = AsyncMock() +        self.delete_messages = AsyncMock() +        self.edit = AsyncMock() +        self.fetch_message = AsyncMock() +        self.invites = AsyncMock() +        self.pins = AsyncMock() +        self.purge = AsyncMock() +        self.send = AsyncMock() +        self.set_permissions = AsyncMock() +        self.trigger_typing = AsyncMock() +        self.webhooks = AsyncMock() + + +# Create a Message instance to get a realistic MagicMock of `discord.Message` +message_data = { +    'id': 1, +    'webhook_id': 431341013479718912, +    'attachments': [], +    'embeds': [], +    'application': 'Python Discord', +    'activity': 'mocking', +    'channel': unittest.mock.MagicMock(), +    'edited_timestamp': '2019-10-14T15:33:48+00:00', +    'type': 'message', +    'pinned': False, +    'mention_everyone': False, +    'tts': None, +    'content': 'content', +    'nonce': None, +} +state = unittest.mock.MagicMock() +channel = unittest.mock.MagicMock() +message_instance = discord.Message(state=state, channel=channel, data=message_data) + + +class MockMessage(AttributeMock, unittest.mock.MagicMock): +    """ +    A MagicMock subclass to mock Message objects. + +    Instances of this class will follow the specifications of `discord.Message` instances. For more +    information, see the `MockGuild` docstring. +    """ + +    attribute_mocktype = unittest.mock.MagicMock + +    def __init__(self, **kwargs) -> None: +        super().__init__(spec=message_instance, **kwargs) +        self.author = MockMember() + +        # `discord.Message` coroutines +        self.ack = AsyncMock() +        self.add_reaction = AsyncMock() +        self.clear_reactions = AsyncMock() +        self.delete = AsyncMock() +        self.edit = AsyncMock() +        self.pin = AsyncMock() +        self.remove_reaction = AsyncMock() +        self.unpin = AsyncMock() diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 766fe17b8..f08239981 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -8,114 +8,8 @@ import discord  from tests import helpers -class MockObjectTests(unittest.TestCase): -    """Tests the mock objects and mixins we've defined.""" -    @classmethod -    def setUpClass(cls): -        cls.hashable_mocks = (helpers.MockRole, helpers.MockMember, helpers.MockGuild) - -    def test_colour_mixin(self): -        """Test if the ColourMixin adds aliasing of color -> colour for child classes.""" -        class MockHemlock(unittest.mock.MagicMock, helpers.ColourMixin): -            pass - -        hemlock = MockHemlock() -        hemlock.color = 1 -        self.assertEqual(hemlock.colour, 1) -        self.assertEqual(hemlock.colour, hemlock.color) - -    def test_hashable_mixin_hash_returns_id(self): -        """Test if the HashableMixing uses the id attribute for hashing.""" -        class MockScragly(unittest.mock.Mock, helpers.HashableMixin): -            pass - -        scragly = MockScragly() -        scragly.id = 10 -        self.assertEqual(hash(scragly), scragly.id) - -    def test_hashable_mixin_uses_id_for_equality_comparison(self): -        """Test if the HashableMixing uses the id attribute for hashing.""" -        class MockScragly(unittest.mock.Mock, helpers.HashableMixin): -            pass - -        scragly = MockScragly(spec=object) -        scragly.id = 10 -        eevee = MockScragly(spec=object) -        eevee.id = 10 -        python = MockScragly(spec=object) -        python.id = 20 - -        self.assertTrue(scragly == eevee) -        self.assertFalse(scragly == python) - -    def test_hashable_mixin_uses_id_for_nonequality_comparison(self): -        """Test if the HashableMixing uses the id attribute for hashing.""" -        class MockScragly(unittest.mock.Mock, helpers.HashableMixin): -            pass - -        scragly = MockScragly(spec=object) -        scragly.id = 10 -        eevee = MockScragly(spec=object) -        eevee.id = 10 -        python = MockScragly(spec=object) -        python.id = 20 - -        self.assertTrue(scragly != python) -        self.assertFalse(scragly != eevee) - -    def test_mock_class_with_hashable_mixin_uses_id_for_hashing(self): -        """Test if the MagicMock subclasses that implement the HashableMixin use id for hash.""" -        for mock in self.hashable_mocks: -            with self.subTest(mock_class=mock): -                instance = helpers.MockRole(role_id=100) -                self.assertEqual(hash(instance), instance.id) - -    def test_mock_class_with_hashable_mixin_uses_id_for_equality(self): -        """Test if MagicMocks that implement the HashableMixin use id for equality comparisons.""" -        for mock_class in self.hashable_mocks: -            with self.subTest(mock_class=mock_class): -                instance_one = mock_class() -                instance_two = mock_class() -                instance_three = mock_class() - -                instance_one.id = 10 -                instance_two.id = 10 -                instance_three.id = 20 - -                self.assertTrue(instance_one == instance_two) -                self.assertFalse(instance_one == instance_three) - -    def test_mock_class_with_hashable_mixin_uses_id_for_nonequality(self): -        """Test if MagicMocks that implement HashableMixin use id for nonequality comparisons.""" -        for mock_class in self.hashable_mocks: -            with self.subTest(mock_class=mock_class): -                instance_one = mock_class() -                instance_two = mock_class() -                instance_three = mock_class() - -                instance_one.id = 10 -                instance_two.id = 10 -                instance_three.id = 20 - -                self.assertFalse(instance_one != instance_two) -                self.assertTrue(instance_one != instance_three) - -    def test_spec_propagation_of_mock_subclasses(self): -        """Test if the `spec` does not propagate to attributes of the mock object.""" -        test_values = ( -            (helpers.MockGuild, "region"), -            (helpers.MockRole, "mentionable"), -            (helpers.MockMember, "display_name"), -            (helpers.MockBot, "owner_id"), -            (helpers.MockContext, "command_failed"), -        ) - -        for mock_type, valid_attribute in test_values: -            with self.subTest(mock_type=mock_type, attribute=valid_attribute): -                mock = mock_type() -                self.assertTrue(isinstance(mock, mock_type)) -                attribute = getattr(mock, valid_attribute) -                self.assertTrue(isinstance(attribute, mock_type.attribute_mocktype)) +class DiscordMocksTests(unittest.TestCase): +    """Tests for our specialized discord.py mocks."""      def test_mock_role_default_initialization(self):          """Test if the default initialization of MockRole results in the correct object.""" @@ -152,28 +46,6 @@ class MockObjectTests(unittest.TestCase):          self.assertEqual(role.guild, "Dino Man")          self.assertTrue(role.hoist) -    def test_mock_role_rejects_accessing_attributes_not_following_spec(self): -        """Test if MockRole throws AttributeError for attribute not existing in discord.Role.""" -        with self.assertRaises(AttributeError): -            role = helpers.MockRole() -            role.joseph - -    def test_mock_role_rejects_accessing_methods_not_following_spec(self): -        """Test if MockRole throws AttributeError for method not existing in discord.Role.""" -        with self.assertRaises(AttributeError): -            role = helpers.MockRole() -            role.lemon() - -    def test_mock_role_accepts_accessing_attributes_following_spec(self): -        """Test if MockRole accepts attribute access for valid attributes of discord.Role.""" -        role = helpers.MockRole() -        role.hoist - -    def test_mock_role_accepts_accessing_methods_following_spec(self): -        """Test if MockRole accepts method calls for valid methods of discord.Role.""" -        role = helpers.MockRole() -        role.edit() -      def test_mock_role_uses_position_for_less_than_greater_than(self):          """Test if `<` and `>` comparisons for MockRole are based on its position attribute."""          role_one = helpers.MockRole(position=1) @@ -223,28 +95,6 @@ class MockObjectTests(unittest.TestCase):          self.assertEqual(member.nick, "Dino Man")          self.assertEqual(member.colour, discord.Colour.default()) -    def test_mock_member_rejects_accessing_attributes_not_following_spec(self): -        """Test if MockMember throws AttributeError for attribute not existing in spec discord.Member.""" -        with self.assertRaises(AttributeError): -            member = helpers.MockMember() -            member.joseph - -    def test_mock_member_rejects_accessing_methods_not_following_spec(self): -        """Test if MockMember throws AttributeError for method not existing in spec discord.Member.""" -        with self.assertRaises(AttributeError): -            member = helpers.MockMember() -            member.lemon() - -    def test_mock_member_accepts_accessing_attributes_following_spec(self): -        """Test if MockMember accepts attribute access for valid attributes of discord.Member.""" -        member = helpers.MockMember() -        member.display_name - -    def test_mock_member_accepts_accessing_methods_following_spec(self): -        """Test if MockMember accepts method calls for valid methods of discord.Member.""" -        member = helpers.MockMember() -        member.mentioned_in() -      def test_mock_guild_default_initialization(self):          """Test if the default initialization of Mockguild results in the correct object."""          guild = helpers.MockGuild() @@ -276,28 +126,6 @@ class MockObjectTests(unittest.TestCase):          self.assertTupleEqual(guild.emojis, (":hyperjoseph:", ":pensive_ela:"))          self.assertEqual(guild.premium_subscription_count, 15) -    def test_mock_guild_rejects_accessing_attributes_not_following_spec(self): -        """Test if MockGuild throws AttributeError for attribute not existing in spec discord.Guild.""" -        with self.assertRaises(AttributeError): -            guild = helpers.MockGuild() -            guild.aperture - -    def test_mock_guild_rejects_accessing_methods_not_following_spec(self): -        """Test if MockGuild throws AttributeError for method not existing in spec discord.Guild.""" -        with self.assertRaises(AttributeError): -            guild = helpers.MockGuild() -            guild.volcyyy() - -    def test_mock_guild_accepts_accessing_attributes_following_spec(self): -        """Test if MockGuild accepts attribute access for valid attributes of discord.Guild.""" -        guild = helpers.MockGuild() -        guild.name - -    def test_mock_guild_accepts_accessing_methods_following_spec(self): -        """Test if MockGuild accepts method calls for valid methods of discord.Guild.""" -        guild = helpers.MockGuild() -        guild.by_category() -      def test_mock_bot_default_initialization(self):          """Tests if MockBot initializes with the correct values."""          bot = helpers.MockBot() @@ -305,10 +133,6 @@ class MockObjectTests(unittest.TestCase):          # The `spec` argument makes sure `isistance` checks with `discord.ext.commands.Bot` pass          self.assertIsInstance(bot, discord.ext.commands.Bot) -        self.assertIsInstance(bot._before_invoke, helpers.AsyncMock) -        self.assertIsInstance(bot._after_invoke, helpers.AsyncMock) -        self.assertEqual(bot.user, helpers.MockMember(name="Python", user_id=123456789)) -      def test_mock_context_default_initialization(self):          """Tests if MockContext initializes with the correct values."""          context = helpers.MockContext() @@ -317,10 +141,213 @@ class MockObjectTests(unittest.TestCase):          self.assertIsInstance(context, discord.ext.commands.Context)          self.assertIsInstance(context.bot, helpers.MockBot) -        self.assertIsInstance(context.send, helpers.AsyncMock)          self.assertIsInstance(context.guild, helpers.MockGuild)          self.assertIsInstance(context.author, helpers.MockMember) +    def test_mocks_allows_access_to_attributes_part_of_spec(self): +        """Accessing attributes that are valid for the objects they mock should succeed.""" +        mocks = ( +            (helpers.MockGuild(), 'name'), +            (helpers.MockRole(), 'hoist'), +            (helpers.MockMember(), 'display_name'), +            (helpers.MockBot(), 'user'), +            (helpers.MockContext(), 'invoked_with'), +            (helpers.MockTextChannel(), 'last_message'), +            (helpers.MockMessage(), 'mention_everyone'), +        ) + +        for mock, valid_attribute in mocks: +            with self.subTest(mock=mock): +                try: +                    getattr(mock, valid_attribute) +                except AttributeError: +                    msg = f"accessing valid attribute `{valid_attribute}` raised an AttributeError" +                    self.fail(msg) + +    @unittest.mock.patch(f'{__name__}.DiscordMocksTests.subTest') +    @unittest.mock.patch(f'{__name__}.getattr') +    def test_mock_allows_access_to_attributes_test(self, mock_getattr, mock_subtest): +        """The valid attribute test should raise an AssertionError after an AttributeError.""" +        mock_getattr.side_effect = AttributeError + +        msg = "accessing valid attribute `name` raised an AttributeError" +        with self.assertRaises(AssertionError, msg=msg): +            self.test_mocks_allows_access_to_attributes_part_of_spec() + +    def test_mocks_rejects_access_to_attributes_not_part_of_spec(self): +        """Accessing attributes that are invalid for the objects they mock should fail.""" +        mocks = ( +            helpers.MockGuild(), +            helpers.MockRole(), +            helpers.MockMember(), +            helpers.MockBot(), +            helpers.MockContext(), +            helpers.MockTextChannel(), +            helpers.MockMessage(), +        ) + +        for mock in mocks: +            with self.subTest(mock=mock): +                with self.assertRaises(AttributeError): +                    mock.the_cake_is_a_lie + +    def test_custom_mock_methods_are_valid_discord_object_methods(self): +        """The `AsyncMock` attributes of the mocks should be valid for the class they're mocking.""" +        mocks = ( +            (helpers.MockGuild, helpers.guild_instance), +            (helpers.MockRole, helpers.role_instance), +            (helpers.MockMember, helpers.member_instance), +            (helpers.MockBot, helpers.bot_instance), +            (helpers.MockContext, helpers.context_instance), +            (helpers.MockTextChannel, helpers.channel_instance), +            (helpers.MockMessage, helpers.message_instance), +        ) + +        for mock_class, instance in mocks: +            mock = mock_class() +            async_methods = ( +                attr for attr in dir(mock) if isinstance(getattr(mock, attr), helpers.AsyncMock) +            ) + +            # spec_mock = unittest.mock.MagicMock(spec=instance) +            for method in async_methods: +                with self.subTest(mock_class=mock_class, method=method): +                    try: +                        getattr(instance, method) +                    except AttributeError: +                        msg = f"method {method} is not a method attribute of {instance.__class__}" +                        self.fail(msg) + +    @unittest.mock.patch(f'{__name__}.DiscordMocksTests.subTest') +    def test_the_custom_mock_methods_test(self, subtest_mock): +        """The custom method test should raise AssertionError for invalid methods.""" +        class FakeMockBot(helpers.AttributeMock, unittest.mock.MagicMock): +            """Fake MockBot class with invalid attribute/method `release_the_walrus`.""" + +            attribute_mocktype = unittest.mock.MagicMock + +            def __init__(self, **kwargs): +                super().__init__(spec=helpers.bot_instance, **kwargs) + +                # Fake attribute +                self.release_the_walrus = helpers.AsyncMock() + +        with unittest.mock.patch("tests.helpers.MockBot", new=FakeMockBot): +            msg = "method release_the_walrus is not a valid method of <class 'discord.ext.commands.bot.Bot'>" +            with self.assertRaises(AssertionError, msg=msg): +                self.test_custom_mock_methods_are_valid_discord_object_methods() + + +class MockObjectTests(unittest.TestCase): +    """Tests the mock objects and mixins we've defined.""" + +    @classmethod +    def setUpClass(cls): +        cls.hashable_mocks = (helpers.MockRole, helpers.MockMember, helpers.MockGuild) + +    def test_colour_mixin(self): +        """Test if the ColourMixin adds aliasing of color -> colour for child classes.""" +        class MockHemlock(unittest.mock.MagicMock, helpers.ColourMixin): +            pass + +        hemlock = MockHemlock() +        hemlock.color = 1 +        self.assertEqual(hemlock.colour, 1) +        self.assertEqual(hemlock.colour, hemlock.color) + +    def test_hashable_mixin_hash_returns_id(self): +        """Test if the HashableMixing uses the id attribute for hashing.""" +        class MockScragly(unittest.mock.Mock, helpers.HashableMixin): +            pass + +        scragly = MockScragly() +        scragly.id = 10 +        self.assertEqual(hash(scragly), scragly.id) + +    def test_hashable_mixin_uses_id_for_equality_comparison(self): +        """Test if the HashableMixing uses the id attribute for hashing.""" +        class MockScragly(unittest.mock.Mock, helpers.HashableMixin): +            pass + +        scragly = MockScragly(spec=object) +        scragly.id = 10 +        eevee = MockScragly(spec=object) +        eevee.id = 10 +        python = MockScragly(spec=object) +        python.id = 20 + +        self.assertTrue(scragly == eevee) +        self.assertFalse(scragly == python) + +    def test_hashable_mixin_uses_id_for_nonequality_comparison(self): +        """Test if the HashableMixing uses the id attribute for hashing.""" +        class MockScragly(unittest.mock.Mock, helpers.HashableMixin): +            pass + +        scragly = MockScragly(spec=object) +        scragly.id = 10 +        eevee = MockScragly(spec=object) +        eevee.id = 10 +        python = MockScragly(spec=object) +        python.id = 20 + +        self.assertTrue(scragly != python) +        self.assertFalse(scragly != eevee) + +    def test_mock_class_with_hashable_mixin_uses_id_for_hashing(self): +        """Test if the MagicMock subclasses that implement the HashableMixin use id for hash.""" +        for mock in self.hashable_mocks: +            with self.subTest(mock_class=mock): +                instance = helpers.MockRole(role_id=100) +                self.assertEqual(hash(instance), instance.id) + +    def test_mock_class_with_hashable_mixin_uses_id_for_equality(self): +        """Test if MagicMocks that implement the HashableMixin use id for equality comparisons.""" +        for mock_class in self.hashable_mocks: +            with self.subTest(mock_class=mock_class): +                instance_one = mock_class() +                instance_two = mock_class() +                instance_three = mock_class() + +                instance_one.id = 10 +                instance_two.id = 10 +                instance_three.id = 20 + +                self.assertTrue(instance_one == instance_two) +                self.assertFalse(instance_one == instance_three) + +    def test_mock_class_with_hashable_mixin_uses_id_for_nonequality(self): +        """Test if MagicMocks that implement HashableMixin use id for nonequality comparisons.""" +        for mock_class in self.hashable_mocks: +            with self.subTest(mock_class=mock_class): +                instance_one = mock_class() +                instance_two = mock_class() +                instance_three = mock_class() + +                instance_one.id = 10 +                instance_two.id = 10 +                instance_three.id = 20 + +                self.assertFalse(instance_one != instance_two) +                self.assertTrue(instance_one != instance_three) + +    def test_spec_propagation_of_mock_subclasses(self): +        """Test if the `spec` does not propagate to attributes of the mock object.""" +        test_values = ( +            (helpers.MockGuild, "region"), +            (helpers.MockRole, "mentionable"), +            (helpers.MockMember, "display_name"), +            (helpers.MockBot, "owner_id"), +            (helpers.MockContext, "command_failed"), +        ) + +        for mock_type, valid_attribute in test_values: +            with self.subTest(mock_type=mock_type, attribute=valid_attribute): +                mock = mock_type() +                self.assertTrue(isinstance(mock, mock_type)) +                attribute = getattr(mock, valid_attribute) +                self.assertTrue(isinstance(attribute, mock_type.attribute_mocktype)) +      def test_async_mock_provides_coroutine_for_dunder_call(self):          """Test if AsyncMock objects have a coroutine for their __call__ method."""          async_mock = helpers.AsyncMock() | 
