python / cpython

The Python programming language
https://www.python.org/
Other
61.16k stars 29.52k forks source link

Add examples for mocking async for and async context manager in unittest.mock docs #81233

Closed tirkarthi closed 2 years ago

tirkarthi commented 5 years ago
BPO 37052
Nosy @cjw296, @ezio-melotti, @voidspace, @asvetlov, @1st1, @phmc, @lisroach, @mariocj89, @miss-islington, @tirkarthi
PRs
  • python/cpython#14660
  • python/cpython#15834
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields: ```python assignee = None closed_at = None created_at = labels = ['type-feature', '3.8', '3.9', 'docs'] title = 'Add examples for mocking async for and async context manager in unittest.mock docs' updated_at = user = 'https://github.com/tirkarthi' ``` bugs.python.org fields: ```python activity = actor = 'pconnell' assignee = 'docs@python' closed = False closed_date = None closer = None components = ['Documentation'] creation = creator = 'xtreak' dependencies = [] files = [] hgrepos = [] issue_num = 37052 keywords = ['patch'] message_count = 5.0 messages = ['343536', '351612', '351614', '351620', '351628'] nosy_count = 11.0 nosy_names = ['cjw296', 'ezio.melotti', 'michael.foord', 'asvetlov', 'docs@python', 'yselivanov', 'pconnell', 'lisroach', 'mariocj89', 'miss-islington', 'xtreak'] pr_nums = ['14660', '15834'] priority = 'normal' resolution = None stage = 'patch review' status = 'open' superseder = None type = 'enhancement' url = 'https://bugs.python.org/issue37052' versions = ['Python 3.8', 'Python 3.9'] ```

    tirkarthi commented 5 years ago

    Since bpo-26467 implemented AsyncMock along with async dunder methods for MagicMock it enables users to mock async for and async with statements. Currently examples of how to use this is present only in tests and would be good to add it to docs. There is a docs page for mock that contains similar cookbook style examples [0] where I hope these can be added. I can raise a PR with these examples if it's okay. Do you think it's worthy enough to add these examples? Any additional examples you find around asyncio and mock that can be documented ?

    An example of mocking async for statement by setting return value for __aiter__ method :

    # aiter_example.py

    import asyncio
    from unittest.mock import MagicMock
    
    mock = MagicMock()
    mock.__aiter__.return_value = range(3)
    
    async def main(): 
        print([i async for i in mock])
    
    asyncio.run(main())
    $ ./python.exe aiter_example.py
    [0, 1, 2]

    An example of mocking async with statement by implementing __aenter and __aexit method. In this example __aenter and __aexit are not called. __aenter and __aexit implementations are tested to have been called in the test at [1]. These tests work since MagicMock is returned during attribute access (mockinstance.entered) which is always True in boolean context under assertTrue. I will raise a separate PR to discuss this since normally while mocking \_enter and __exit the class's __enter and __exit are not used as a side_effect for the mock calls unless they are set explicitly.

    # aenter_example.py

    import asyncio
    from unittest.mock import MagicMock
    
    class WithAsyncContextManager:
    
        async def __aenter__(self, *args, **kwargs):
            return self
    
        async def __aexit__(self, *args, **kwargs):
            pass
    
    instance = WithAsyncContextManager()
    mock_instance = MagicMock(instance)
    
    async def main():
        async with mock_instance as result:
            print("entered")
    
    asyncio.run(main())

    ./python.exe aenter_example.py entered

    [0] https://docs.python.org/3/library/unittest.mock-examples.html [1] https://github.com/python/cpython/blob/47dd2f9fd86c32a79e77fef1fbb1ce25dc929de6/Lib/unittest/test/testmock/testasync.py#L306

    lisroach commented 4 years ago

    I think this is a great addition! Ezio and I were chatting about trying to add an example where the child mocks are also AsyncMocks, since by default they will be MagicMocks. Adding him to nosy.

    miss-islington commented 4 years ago

    New changeset c8dfa7333d6317d7cd8c5c7366023f5a668e3f91 by Miss Islington (bot) (Xtreak) in branch 'master': bpo-37052: Add examples for mocking async iterators and context managers (GH-14660) https://github.com/python/cpython/commit/c8dfa7333d6317d7cd8c5c7366023f5a668e3f91

    miss-islington commented 4 years ago

    New changeset ab74e52f768be5048faf2a11e78822533afebcb7 by Miss Islington (bot) in branch '3.8': bpo-37052: Add examples for mocking async iterators and context managers (GH-14660) https://github.com/python/cpython/commit/ab74e52f768be5048faf2a11e78822533afebcb7

    tirkarthi commented 4 years ago

    I will open a separate PR as discussed around mocking a class with an async method which is patched with AsyncMock. Meanwhile a method that returns a coroutine is patched with a MagicMock and needs to be explicitly mocked with an AsyncMock as new in the patch call. The original example in Zulip is as below showing the difference.

    from unittest.mock import AsyncMock, patch
    import asyncio
    
    async def foo():
        pass
    
    async def post(url):
        pass
    
    class Response:
    
        async def json(self):
            pass
    
        def sync_json(self):
            return foo() # Returns a coroutine which should be awaited to get the result
    
    async def main():
        # post function is an async function and hence AsyncMock is returned.
        with patch(f"{__name__}.post", return_value={'a': 1}) as m:
            print(await post("http://example.com"))
    
        # The json method call is a coroutine whose return_value is set with the dictionary
        # json is an async function and hence during patching here m is an AsyncMock
        response = Response()
        with patch.object(response, 'json', return_value={'a': 1}):
            print(await response.json())
    
        # sync_json returns a coroutine and not an async def itself. So it's mocked as MagicMock
        # by patch.object and we need to pass an explicit callable as AsyncMock to make sure it's
        # awaitable
        response = Response()
        with patch.object(response, 'sync_json', AsyncMock(return_value={'a': 1})):
            print(await response.sync_json())
    
    asyncio.run(main())
    kumaraditya303 commented 2 years ago

    This seems fixed by #14660