From e4e01cd5388da19435637353e711c2feab5a0e59 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Mon, 14 Oct 2019 22:13:22 +0200 Subject: 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. --- tests/test_helpers.py | 385 +++++++++++++++++++++++++++----------------------- 1 file changed, 206 insertions(+), 179 deletions(-) (limited to 'tests/test_helpers.py') 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 " + 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() -- cgit v1.2.3