-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
Improve documentation for mocks #16859
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
Conversation
5ee5c6a
to
8a301b2
Compare
|
||
>>> class AsyncContextManager: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this documentation made more clear by removing this example? The goal here was to show how someone could mock an asynchronous context manager that they wrote using MagicMock (or AsyncMock). You've changed it to show MagicMock can act as an asynchronous context manager, which I think without the context of mocking a real object doesn't demonstrate a useful scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The confusing part is that the CM class is defined together with the code that mocks it. The user might also want to mock a CM that they didn't define, but obtain through a call (e.g. to open
).
In the first case, it might be enough to imply the existence of the CM by doing from mymodule import AsyncContextManager
, and describing how MagicMock(AsyncContextManager())
automatically find and mocks __aenter__
and __aexit__
with AsyncMock
s.
In the second case, what Elena did is OK unless we want a more realistic example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion defining the AsyncContextManager
class here is kind of useless. All of its methods will be mocked by MagicMock, so we might as well forget of its existence, or am I missing something here? What I mean is that mock_instance = MagicMock(AsyncContextManager())
is equivalent to mock_instance = MagicMock()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@elenaoat I don't agree that because the custom context manager is mocked it is not relevant. Mock is used to mock objects that exist in code to have different behavior for the sake of testing. I cannot come up with a use case for a mock object as an asynchronous context manager that is not specced with any real piece of code. What problem is it solving?
Maybe the test could be more verbose by adding a return value or side effect on the mock object, but I don't think it makes sense to ever use it without a spec.
And mock_instance = MagicMock(AsyncContextManager())
is not the same as mock_instance =MagicMock()
off the bat:
>>> dir(MagicMock())
['assert_any_call', 'assert_called', 'assert_called_once', 'assert_called_once_with', 'assert_called_with', 'assert_has_calls', 'assert_not_called', 'attach_mock', 'call_args', 'call_args_list', 'call_count', 'called', 'configure_mock', 'method_calls', 'mock_add_spec', 'mock_calls', 'reset_mock', 'return_value', 'side_effect']
>>> dir(MagicMock(AsyncContextManager))
['__aenter__', '__aexit__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'assert_any_call', 'assert_called', 'assert_called_once', 'assert_called_once_with', 'assert_called_with', 'assert_has_calls', 'assert_not_called', 'attach_mock', 'call_args', 'call_args_list', 'call_count', 'called', 'configure_mock', 'method_calls', 'mock_add_spec', 'mock_calls', 'reset_mock', 'return_value', 'side_effect']
I agree that it is weird to define them together in the same file, but I think that might be a limitation of doctest. I assumed we would not be able to import AsyncContextManager
without it failing, because mymodule
doesn't actually exist? I might be wrong about how this works though. If we can import it I think that is a good idea, maybe make the name more explicit like MyCustomAsyncContextManager
.
If we can't import it I still think it is better to build it in here then exclude it all together, maybe we can add a comment block around it, implying it is in a different file without actually being in a different file?
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
@@ -208,15 +208,13 @@ If you need an attribute setting on your mock, just do it: | |||
3 | |||
|
|||
Sometimes you want to mock up a more complex situation, like for example | |||
``mock.connection.cursor().execute("SELECT 1")``. If we wanted this call to | |||
return a list, then we have to configure the result of the nested call. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rephrase this as If we wanted this call to return a list, then we have to specify the return value of the two calls.
.
I think here "nested" is used incorrectly, because even though the mocks might be considered nested, the calls in the expressions are not. If we want to make this more explicit, the two calls (cursor() and execute()) could be mentioned explicitly.
|
||
>>> class AsyncContextManager: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The confusing part is that the CM class is defined together with the code that mocks it. The user might also want to mock a CM that they didn't define, but obtain through a call (e.g. to open
).
In the first case, it might be enough to imply the existence of the CM by doing from mymodule import AsyncContextManager
, and describing how MagicMock(AsyncContextManager())
automatically find and mocks __aenter__
and __aexit__
with AsyncMock
s.
In the second case, what Elena did is OK unless we want a more realistic example.
>>> asyncio.run(main()) | ||
>>> mock_instance.__aenter__.assert_awaited_once() | ||
>>> mock_instance.__aexit__.assert_awaited_once() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these 2 lines should be kept.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Mock doesn't have these two methods, that's why it will fail.
ea91526
to
c06ebdd
Compare
Add more meaningful examples and correct a few grammar mistakes.
* there is no need in separate intermediary call patching * the example with mocking asynchronous context manager wasn't clear: why do we create a separate class with __aexit__ and __aenter__ if MagicMock and AsyncMock covers this already. * over use -> overuse - grammar mistake.
* Remove example * Reformulate explanation for chained calls
c06ebdd
to
19dd9d9
Compare
Thanks for the ping @csabella this got buried for me. I'd like to hear @tirkarthi opinions on this too, since he was a significant author for these docs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies, I thought I submitted the review. I didn't know it was pending.
We can use :data:`call` to construct the set of calls in a "chained call" like | ||
this for easy assertion afterwards: | ||
>>> db = Mock() | ||
>>> db.connection.cursor.return_value.execute.return_value = ['foo'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though the earlier example was slightly verbose I think it's clear on setting nested mock return values. Personally I don't prefer much nested chains in the docs so I would suggest keeping the old example and then adding a note with this line that nested mocks return value could be set with this one liner as alternative.
You can set attributes on the return value too: | ||
|
||
>>> mock.return_value.attribute = 'some random attribute value' | ||
>>> mock.return_value.attribute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of demonstrating the attribute to be present on the return_value attribute itself we the below reads more useful showing the attribute to be set on the return value itself as call is made.
>>> mock.return_value.attribute | |
>>> mock().attribute |
>>> str(mock) | ||
'foobarbaz' | ||
>>> mock.__str__.assert_called_with() | ||
>>> len(mock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest keeping the old example setting return_value
on __str__
though there is a different example below setting __str__.return_value
with a mock. May I know the purpose of changing it to len
? Is it show the default value for some magic methods?
See also https://bugs.python.org/issue40406 with reports about the nested mocking workflow to be confusing, |
@elenaoat: are you planning on following up this PR? |
I'm going to close this PR as it has been pending over six months. Thanks @elenaoat and reviewers for your effort. |
Add more meaningful examples and correct a few grammar mistakes.