diff options
author | 2019-10-29 10:04:37 +0100 | |
---|---|---|
committer | 2019-10-30 22:21:30 +0100 | |
commit | 618ba6a523dababde230382e1965ecc89f23aaf5 (patch) | |
tree | 52e76628c371b46000bc12afeabd59ae38e33861 /tests/test_helpers.py | |
parent | Change generation of child mocks (diff) |
Enhance custom mock helpers
I have enhanced the custom mocks defined in `tests/helpers.py` in a
couple of important ways.
1. Automatically create AsyncMock attributes using `inspect`
Our previous approach, hard-coding AsynckMock attributes for all the
coroutine function methods defined for the class we are trying to
mock is prone to human error and not resilient against changes
introduced in updates of the library we are using.
Instead, I have now created a helper method in our `CustomMockMixin`
(formerly `GetChildMockMixin`) that automatically inspects the spec
instance we've passed for `coroutine functions` using the `inspect`
module. It then sets the according attributes with instances of the
AsyncMock class.
There is one caveat: `discord.py` very rarely defines regular methods
that return a coroutine object. Since the returned coroutine should
still be awaited, these regular methods should also be mocked with an
AsyncMock. However, since they are regular methods, `inspect` does
not detect them and they have to be added manually. (The only case of
this I've found so far is `Client.wait_for`.)
2. Properly set special attributes using `kwargs.get`
As we want attributes that point to other discord.py objects to use
our custom mocks (.e.g, `Message.author` should use `MockMember`),
the `__init__` method of our custom mocks make sure to correctly
instantiate these attributes.
However, the way we previously did that means we can't instantiate
the custom mock with a mock instance we provide, since this special
instantiation would overwrite the custom object we'd passed. I've
solved this by using `kwargs.get`, with a new mock as the default
value. This makes sure we only create a new mock if we didn't pass
a custom one:
```py
class MockMesseage:
def __init__(self, **kwargs):
self.author = kwargs.get('author', MockMember())
```
As you can see, we will only create a new MockMember if we did not
pass an `author` argument.
3. Factoring out duplicate lines
Since our `CustomMockMixin` is a parent to all of our custom mock
types, it makes sense to use it to factor out common code of all of
our custom mocks.
I've made the following changes:
- Set a default child mock type in the mixin.
- Create an `__init__` that takes care of the `inspect` of point 1
This means we won't have to repeat this in all of the child classes.
4. Three new Mock types: Emoji, PartialEmoji, and Reaction
I have added three more custom mocks:
- MockEmoji
- MockPartialEmoji
- MockReaction
Diffstat (limited to 'tests/test_helpers.py')
-rw-r--r-- | tests/test_helpers.py | 58 |
1 files changed, 54 insertions, 4 deletions
diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 62007ff4e..2b58634dd 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -221,7 +221,7 @@ class DiscordMocksTests(unittest.TestCase): @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.GetChildMockMixin, unittest.mock.MagicMock): + class FakeMockBot(helpers.CustomMockMixin, unittest.mock.MagicMock): """Fake MockBot class with invalid attribute/method `release_the_walrus`.""" child_mock_type = unittest.mock.MagicMock @@ -331,9 +331,9 @@ class MockObjectTests(unittest.TestCase): self.assertFalse(instance_one != instance_two) self.assertTrue(instance_one != instance_three) - def test_get_child_mock_mixin_accepts_mock_seal(self): - """The `GetChildMockMixin` should support `unittest.mock.seal`.""" - class MyMock(helpers.GetChildMockMixin, unittest.mock.MagicMock): + def test_custom_mock_mixin_accepts_mock_seal(self): + """The `CustomMockMixin` should support `unittest.mock.seal`.""" + class MyMock(helpers.CustomMockMixin, unittest.mock.MagicMock): child_mock_type = unittest.mock.MagicMock pass @@ -351,6 +351,10 @@ class MockObjectTests(unittest.TestCase): (helpers.MockMember, "display_name"), (helpers.MockBot, "owner_id"), (helpers.MockContext, "command_failed"), + (helpers.MockMessage, "mention_everyone"), + (helpers.MockEmoji, 'managed'), + (helpers.MockPartialEmoji, 'url'), + (helpers.MockReaction, 'me'), ) for mock_type, valid_attribute in test_values: @@ -360,6 +364,52 @@ class MockObjectTests(unittest.TestCase): attribute = getattr(mock, valid_attribute) self.assertTrue(isinstance(attribute, mock_type.child_mock_type)) + def test_extract_coroutine_methods_from_spec_instance_should_extract_all_and_only_coroutines(self): + """Test if all coroutine functions are extracted, but not regular methods or attributes.""" + class CoroutineDonor: + def __init__(self): + self.some_attribute = 'alpha' + + async def first_coroutine(): + """This coroutine function should be extracted.""" + + async def second_coroutine(): + """This coroutine function should be extracted.""" + + def regular_method(): + """This regular function should not be extracted.""" + + class Receiver: + pass + + donor = CoroutineDonor() + receiver = Receiver() + + helpers.CustomMockMixin._extract_coroutine_methods_from_spec_instance(receiver, donor) + + self.assertIsInstance(receiver.first_coroutine, helpers.AsyncMock) + self.assertIsInstance(receiver.second_coroutine, helpers.AsyncMock) + self.assertFalse(hasattr(receiver, 'regular_method')) + self.assertFalse(hasattr(receiver, 'some_attribute')) + + @unittest.mock.patch("builtins.super", new=unittest.mock.MagicMock()) + @unittest.mock.patch("tests.helpers.CustomMockMixin._extract_coroutine_methods_from_spec_instance") + def test_custom_mock_mixin_init_with_spec(self, extract_method_mock): + """Test if CustomMockMixin correctly passes on spec/kwargs and calls the extraction method.""" + spec = "pydis" + + helpers.CustomMockMixin(spec=spec) + + extract_method_mock.assert_called_once_with(spec) + + @unittest.mock.patch("builtins.super", new=unittest.mock.MagicMock()) + @unittest.mock.patch("tests.helpers.CustomMockMixin._extract_coroutine_methods_from_spec_instance") + def test_custom_mock_mixin_init_without_spec(self, extract_method_mock): + """Test if CustomMockMixin correctly passes on spec/kwargs and calls the extraction method.""" + helpers.CustomMockMixin() + + extract_method_mock.assert_not_called() + 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() |