Python's unittest fails to pass in parameter "missing 1 required positional argument: 'ctx'" for Discord bot testing?

Viewed 602

I am trying to write a simple test case for one of my Discord.py commands:

https://github.com/NobleUplift/Hierarchies/blob/master/cogs/HierarchyManagement.py#L32

The function definition is quite simple:

async def list(self, ctx: discord.ext.commands.Context):

And I am trying to write a unit test that replaces ctx.send with print for now, to validate the output of my Discord command, with the eventual goal to add asserts that validate the output of the command:

Hierarchies: 
 • us-army
 • admins

Here is my full test case:

import unittest
from unittest.mock import Mock
from unittest.mock import patch

from cogs.HierarchyManagement import HierarchyManagement

class test_Hierarchies(unittest.IsolatedAsyncioTestCase):
    async def test_list(self):
        bot = Mock() # commands.Bot()
        ctx = Mock() # discord.ext.commands.Context()
        ctx.message.content = '^list'
        ctx.message.guild.id = 870166294127837205
        ctx.author.mention = '<@368187588340875284>'
        ctx.author.name = 'NobleUplift'
        ctx.author.discriminator = '1038'
        ctx.send = lambda retval: print(retval)
        print(ctx.message.guild.id)
        hierarchyManagement = HierarchyManagement(bot)
        print(ctx)
        print(hierarchyManagement)
        await hierarchyManagement.list(ctx)

if __name__ == '__main__':
    unittest.main()

And this is the error I receive:

Error
Traceback (most recent call last):
  File "C:\Program Files\Python38\lib\unittest\case.py", line 60, in testPartExecutor
    yield
  File "C:\Program Files\Python38\lib\unittest\case.py", line 676, in run
    self._callTestMethod(testMethod)
  File "C:\Program Files\Python38\lib\unittest\async_case.py", line 65, in _callTestMethod
    self._callMaybeAsync(method)
  File "C:\Program Files\Python38\lib\unittest\async_case.py", line 88, in _callMaybeAsync
    return self._asyncioTestLoop.run_until_complete(fut)
  File "C:\Program Files\Python38\lib\asyncio\base_events.py", line 616, in run_until_complete
    return future.result()
  File "C:\Program Files\Python38\lib\unittest\async_case.py", line 102, in _asyncioLoopRunner
    ret = await awaitable
  File "Hierarchies\cogs\test_HierarchyManagement.py", line 21, in test_list
    await hierarchyManagement.list(ctx)
  File "Python38\site-packages\discord\ext\commands\core.py", line 374, in __call__
    return await self.callback(*args, **kwargs)
TypeError: list() missing 1 required positional argument: 'ctx'

Why is it telling me that it requires ctx when I am quite clearly passing in the ctx variable?

1 Answers

The issue here is when you define a discord.py command, you'll need to use the @command() decorator. The decorator automatically does some conversions and redefines HierarchyManagement.list to a discord.Command object representing the list command. Since decorators are ran without the instance self, your list method gets stored as a self-less function at hierarchyManagement.list._callback.

When you're calling hierarchyManagement.list() you're actually calling the commands.Command.__call__() dunder method which redirects to hierarchyManagement.list._callback(). And we established earlier that the callback doesn't have the self reference.

This is why, you'll need to do:

await hierarchyManagement.list(hierarchyManagement, ctx)

The reason why it works "magically" when discord.py runs it is because during command argument parsing, they inject the self into the function callback at runtime. Since calling hierarchyManagement.list direct skips all the parsing, the self wasn't injected in your case.

As an aside, ctx.send is a coroutine, but when you defined it in mock ctx.send = lambda retval: print(retval) its a regular function, this will raise an error later on.

Related