Skip to content

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

Closed
wants to merge 5 commits into from
Closed

Conversation

elenaoat
Copy link
Contributor

Add more meaningful examples and correct a few grammar mistakes.


>>> class AsyncContextManager:
Copy link
Contributor

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.

Copy link
Member

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 AsyncMocks.

In the second case, what Elena did is OK unless we want a more realistic example.

Copy link
Contributor Author

@elenaoat elenaoat Oct 27, 2019

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()

Copy link
Contributor

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?

@bedevere-bot
Copy link

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 I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@@ -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.
Copy link
Member

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:
Copy link
Member

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 AsyncMocks.

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()
Copy link
Member

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.

Copy link
Contributor Author

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.

@elenaoat elenaoat force-pushed the improve-mock-docs branch 2 times, most recently from ea91526 to c06ebdd Compare October 28, 2019 01:41
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
@lisroach
Copy link
Contributor

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.

@csabella csabella requested a review from tirkarthi January 12, 2020 13:39
Copy link
Member

@tirkarthi tirkarthi left a 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']
Copy link
Member

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
Copy link
Member

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.

Suggested change
>>> mock.return_value.attribute
>>> mock().attribute

>>> str(mock)
'foobarbaz'
>>> mock.__str__.assert_called_with()
>>> len(mock)
Copy link
Member

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?

@tirkarthi
Copy link
Member

See also https://bugs.python.org/issue40406 with reports about the nested mocking workflow to be confusing,

@AlexWaygood AlexWaygood changed the title bpo: Improve documentation for mocks Improve documentation for mocks Oct 30, 2022
@erlend-aasland
Copy link
Contributor

@elenaoat: are you planning on following up this PR?

@erlend-aasland erlend-aasland added the pending The issue will be closed if no feedback is provided label Mar 31, 2024
@willingc
Copy link
Contributor

I'm going to close this PR as it has been pending over six months.

Thanks @elenaoat and reviewers for your effort.

@willingc willingc closed this Oct 31, 2024
@AA-Turner AA-Turner removed the pending The issue will be closed if no feedback is provided label Apr 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Todo
Development

Successfully merging this pull request may close these issues.

10 participants