Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explain how to properly mock user-defined __call__ #131383

Open
clafoutis42 opened this issue Mar 17, 2025 · 9 comments
Open

Explain how to properly mock user-defined __call__ #131383

clafoutis42 opened this issue Mar 17, 2025 · 9 comments
Labels
docs Documentation in the Doc dir

Comments

@clafoutis42
Copy link

clafoutis42 commented Mar 17, 2025

Bug report

Bug description:

Reproduce

from unittest.mock import MagicMock

class A:
    def __init__(self, a):
        ...

    def __call__(self, b, c):
        ...

mocker = MagicMock(spec=A)
mocker(21, 42)
mocker.assert_called_once_with(21, 42)

Current Behavior

TypeError                                 Traceback (most recent call last)
TypeError: too many positional arguments

The above exception was the direct cause of the following exception:

AssertionError                            Traceback (most recent call last)
Cell In[6], line 10
      8 mocker = MagicMock(spec=A)
      9 mocker(21, 42)
---> 10 mocker.assert_called_once_with(21, 42)

File ~/.pyenv/versions/3.12.2/lib/python3.12/unittest/mock.py:956, in NonCallableMock.assert_called_once_with(self, *args, **kwargs)
    951     msg = ("Expected '%s' to be called once. Called %s times.%s"
    952            % (self._mock_name or 'mock',
    953               self.call_count,
    954               self._calls_repr()))
    955     raise AssertionError(msg)
--> 956 return self.assert_called_with(*args, **kwargs)

File ~/.pyenv/versions/3.12.2/lib/python3.12/unittest/mock.py:944, in NonCallableMock.assert_called_with(self, *args, **kwargs)
    942 if actual != expected:
    943     cause = expected if isinstance(expected, Exception) else None
--> 944     raise AssertionError(_error_message()) from cause

AssertionError: expected call not found.
Expected: mock(21, 42)
  Actual: mock(21, 42)

Expected behaviour

It succeeds

Investigation

When passing a class that implements the __call__ method as spec to a mock instance it takes the it takes the signature of the __init__ which if it is the intended behavior is quite confusing.

CPython versions tested on:

3.12, 3.13

Operating systems tested on:

macOS, Linux

@clafoutis42 clafoutis42 added the type-bug An unexpected behavior, bug, or error label Mar 17, 2025
@picnixz picnixz added the stdlib Python modules in the Lib dir label Mar 17, 2025
@picnixz
Copy link
Member

picnixz commented Mar 17, 2025

AFAICT, __call__ is not supported as a magic method (at least it's not documented in https://docs.python.org/3/library/unittest.mock.html#mocking-magic-methods). So I don't know if it's a bug or not.

@picnixz picnixz added the pending The issue will be closed if no feedback is provided label Mar 17, 2025
@clafoutis42
Copy link
Author

AFAICT, __call__ is not supported as a magic method (at least it's not documented in https://docs.python.org/3/library/unittest.mock.html#mocking-magic-methods). So I don't know if it's a bug or not.

Indeed, that's something I missed while browsing the doc. Considering this I would understand if this is not considered as a bug.

Either way, I also have a workaround:

from unittest.mock import MagicMock

class A:
    def __init__(self, a):
        ...

    def __call__(self, b, c):
        ...

mocker = MagicMock(spec=A, side_effect=MagicMock())
mocker(21, 42)
mocker.side_effect.assert_called_once_with(21, 42)

It's not that pretty but does the trick if you really have to mock a callable instance

@picnixz
Copy link
Member

picnixz commented Mar 17, 2025

@cjw296 do you think it's a legitimate feature request or would it be impossible to make it work properly?^

@cjw296
Copy link
Contributor

cjw296 commented Mar 18, 2025

Evidently something is not right here :-)

I've only scanned this quickly, but:

  • I can't think of a logical reason why __call__ shouldn't be supported, if there's a pragmatic one, I'm unaware of it
  • The current behaviour if confusing, but, tbh, I've always found the spec'ing side of mock pretty confusing.

What happens if you do:

mocker = MagicMock(spec=A(1))`

?

@clafoutis42
Copy link
Author

What happens if you do:

mocker = MagicMock(spec=A(1))`
?

Then the test passes and _spec_signature is correct.

@picnixz picnixz removed the pending The issue will be closed if no feedback is provided label Mar 18, 2025
@cjw296
Copy link
Contributor

cjw296 commented Mar 18, 2025

Okay, so if anything this feels like a documentation issue.
If you do mocker = MagicMock(spec=A), your spec is the type, so you're actually mocking out the class, and when you call a class, it's __init__ that gets executed, if you do mocker = MagicMock(spec=A(1)), then you're mocking the instance, and when you call an instance __call__ is the method used.

Maybe you could work up a documentation PR with changes that would have helped you here?

@cjw296
Copy link
Contributor

cjw296 commented Mar 18, 2025

Also:

TypeError                                 Traceback (most recent call last)
TypeError: too many positional arguments

This is the important exception. Why is your test going on to do mocker.assert_called_once_with(21, 42) if an exception has already occurred?

@picnixz picnixz changed the title bug in unittest.mock when mocking class implementing __call__ method Explain how to properly mock user-defined __call__ Mar 18, 2025
@picnixz picnixz added docs Documentation in the Doc dir and removed type-bug An unexpected behavior, bug, or error stdlib Python modules in the Lib dir labels Mar 18, 2025
@clafoutis42
Copy link
Author

I completely understand that this could be a documentation issue, especially since in the case of MagicMock the mock is _spec_as_instance is not set and is then False which seems to be the reason why the signature is that of the __init__ and not the __call__ method.
I dig in the _get_signature_object and I can tell that it's the intended behavior. It would be good to reflect that in the documentation indeed.

This is the important exception. Why is your test going on to do mocker.assert_called_once_with(21, 42) if an exception has already occurred?

I guess that's the part that got me confused in the first place, it only raises this exception when calling assert_called_once_with (or assert_called_with and assert_has_calls). Calling the mocker itself does not raise any issues at all and then the error states that the calls are different while showing the exact same calls.
So the bug might in fact be right here?

@cjw296
Copy link
Contributor

cjw296 commented Mar 20, 2025

I guess that's the part that got me confused in the first place, it only raises this exception when calling assert_called_once_with (or assert_called_with and assert_has_calls). Calling the mocker itself does not raise any issues at all and then the error states that the calls are different while showing the exact same calls.
So the bug might in fact be right here?

Could you submit a minimal reproducer for this as a test case in a PR to this repo?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Documentation in the Doc dir
Projects
Status: Todo
Status: Todo
Development

No branches or pull requests

3 participants