Python Mocking with unitest.mock: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
 
(9 intermediate revisions by the same user not shown)
Line 1: Line 1:
=External=
* https://docs.python.org/3/library/unittest.mock.html#module-unittest.mock
=Internal=
=Internal=
* [[Python_Module_unittest#unittest.mock|unittest Module]]
* [[Python_Module_unittest#unittest.mock|unittest Module]]
Line 10: Line 8:


By default, a <code>Mock</code> accept any invocations into it.
By default, a <code>Mock</code> accept any invocations into it.
=<tt>MagicMock</tt>=
Difference between <code>[[#Mock|Mock]]</code> and <code>[[#MagicMock|MagicMock]]</code>.


=<tt>patch() and @patch</tt>=
=<tt>patch() and @patch</tt>=
Line 73: Line 67:
How to simulate different return values for a mocked function depending on an argument value?
How to simulate different return values for a mocked function depending on an argument value?
</font>
</font>
===Mocking a Property===
{{External|https://docs.python.org/3/library/unittest.mock.html#unittest.mock.PropertyMock}}
Option 1:
<syntaxhighlight lang='py'>
mock_label = Mock(Label)
type(mock_label).name = PropertyMock(return_value='a')
assert mock_label.name == 'a'
</syntaxhighlight>
Option 2:
<syntaxhighlight lang='py'>
from unittest.mock import patch, PropertyMock
class A:
    @property
    def property_a(self):
        return "pa"
with patch('__main__.A.property_a', new_callable=PropertyMock) as mock_property:
    mock_property.return_value = 'mocked pa'
    a = A()
    assert a.property_a == 'mocked pa'
</syntaxhighlight>


===Mocking a Method===
===Mocking a Method===
Line 111: Line 81:
</syntaxhighlight>
</syntaxhighlight>
====Mocking an Exception====
====Mocking an Exception====
<syntaxhighlight lang='py'>
Moved to: {{Internal|Python_Mocking_with_unitest.mock_2#Simulating_Throwing_an_Exception_Irrespective_of_the_Arguments_it_was_Called_With|Python Mocking with unitest.mock 2 &#124; Simulating Throwing an Exception Irrespective of the Arguments it was Called With}}
mock_repository.get_contents.side_effect=UnknownObjectException(404, None, None)
# equivalent:
mock_repository.get_contents = Mock(side_effect=UnknownObjectException(404, None, None) )
</syntaxhighlight>


===Mocking a Regular Module Function===
===Mocking a Regular Module Function===
Line 167: Line 133:
       # to testing with 'subprocess.run' mocked ...
       # to testing with 'subprocess.run' mocked ...
</syntaxhighlight>
</syntaxhighlight>
===Asserting Invocations on Mock===
<syntaxhighlight lang='py'>
mock.assert_called_once_with(
  "some concrete arg 1",
  unittest.mock.ANY)
</syntaxhighlight>
<font color=darkkhaki>TO further document: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.ANY</font>
<syntaxhighlight lang='py'>
mock = ...
mock.call_count # return the number of times the mock was called into, as an int
</syntaxhighlight>
====<tt>call_args_list</tt>====
<syntaxhighlight lang='py'>
mock = ...
mock.call_args_list
</syntaxhighlight>
<code>call_args_list</code> is a list of call arguments for all calls that were made on the mock. The list length is equal with <code>mock.call_count</code>
Call arguments for each call can be obtained with the index operator []:
<syntaxhighlight lang='py'>
args_for_first_call = mock.call_args_list[0]
args_for_second_call = mock.call_args_list[1]
...
</syntaxhighlight>
Positional arguments are maintained in a tuple and can be obtained with the <code>args</code> property. It returns a tuple, which may be empty:
<syntaxhighlight lang='py'>
mock.call_args_list[0].args
</syntaxhighlight>
Named arguments are maintained in a dictionary, which can be obtained with the <code>kwargs</code> property. It returns a dictionary, which may be empty:
<syntaxhighlight lang='py'>
mock.call_args_list[0].kwargs
mock.call_args_list[0].kwargs['some_arg']
</syntaxhighlight>
====<tt>method_calls</tt>====
I've seen this empty, use <code>mock_calls</code>.
====<tt>mock_calls</tt>====
Document this.


=Iteration 2=
=Iteration 2=


{{Internal|Python Mocking with unitest.mock 2|Python Mocking with unitest.mock 2}}
{{Internal|Python Mocking with unitest.mock 2|Python Mocking with unitest.mock 2}}

Latest revision as of 18:49, 18 August 2023

Internal

Overview

The unittest.mock library allows replacing parts of the system under test with mock objects and make assertion about how they are accessed and used.

Mock

https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock

By default, a Mock accept any invocations into it.

patch() and @patch

patch can be used as a function decorator, class decorator or context manager. Inside the body of the decorated function or the with statement, first argument ("target") is patched with the instance provided as the "new" argument. When the function/with statement exits, the patch is undone.

def patch(target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, *, unsafe=False, **kwargs):
  ...

"target" should be a string in the form 'package.module.ClassName'. The "target" is imported and the specified object replaced with the "new" object, so the "target" must be importable from the environment you are calling patch from. The target is imported when the decorated function is executed, not at decoration time.

If the "new" argument is omitted, the target is replaced with an AsyncMock if the patched object is an async function or a MagicMock otherwise. If patch is used as a decorator and "new" is omitted, the created mock is passed in as an extra argument to the decorated function. If "patch" is used as a context manager the created mock is returned by the context manager.

Usage:

@patch("aws_helper.boto3.session")
def _inject_mock_sessions(mock_target=None):
     mock_target.Session = Mock()
     ...

What is "aws_helper.boto3.session" in this situation? What does represent it, as a target? It is a package. If yes, what does mock_boto3_session.Session = Mock() do?

Sentinel

TODEPLETE

https://realpython.com/python-mock-library/

Mocking

To mock a class and a method of that class:

from unittest.mock import Mock


class SomeClass:
    def __init__(self, state):
        self.state = state

    def some_method(self):
        return self.state


sc = SomeClass('A')
assert 'A' == sc.some_method()

sc_mock = Mock(SomeClass)
sc_mock.some_method = Mock(return_value='blah')

assert 'blah' == sc_mock.some_method()


How to simulate different return values for a mocked function depending on an argument value?

Mocking a Method

from unittest.mock import patch

class A:
    def method_a(self):
        return "ma"

with patch.object(A, 'method_a', return_value='mocked ma') as mock_method:
    a = A()
    assert a.method_a() == 'mocked ma'

Mocking an Exception

Moved to:

Python Mocking with unitest.mock 2 | Simulating Throwing an Exception Irrespective of the Arguments it was Called With

Mocking a Regular Module Function

Use patch() as a context manager. Inside the with statement, the `target` (the first argument of patch()) is patched with a `new` object. When the with statement exits, the patch is undone. If the 'new' is committed, the target is replaced with a MagicMock, and the created mock is returned by the context manager.

import subprocess
from unittest.mock import patch, Mock

class MockCompletedProcess:
    @property
    def stdout(self):
        return "mock stdout"

with patch('subprocess.run', new=Mock(return_value=MockCompletedProcess())) as run_interceptor:
    result = subprocess.run('ls', capture_output=True, check=True, shell=True)
    assert result.stdout == 'mock stdout'
    result = subprocess.run('echo', capture_output=False, check=False, shell=True)
    assert result.stdout == 'mock stdout'

assert run_interceptor.call_count == 2
assert run_interceptor.call_args_list[0].args[0] == 'ls'
assert run_interceptor.call_args_list[0].kwargs['capture_output'] is True
assert run_interceptor.call_args_list[0].kwargs['check'] is True
assert run_interceptor.call_args_list[0].kwargs['shell'] is True
assert run_interceptor.call_args_list[1].args[0] == 'echo'
assert run_interceptor.call_args_list[1].kwargs['capture_output'] is False
assert run_interceptor.call_args_list[1].kwargs['check'] is False
assert run_interceptor.call_args_list[1].kwargs['shell'] is True

Alternative setting where we intercept the arguments and control output depending on arguments:

class MockCompletedProcess:
    def __init__(self, *args, **kwargs):
        self._stdout = None
        command = args[0]
        if command.startswith('spin account get'):
            self._stdout = '{"name": "test"}'
        elif command.startswith('spin pt save') or command.startswith('spin pipeline save'):
            self._stdout = 'Pipeline save succeeded'
        else:
            self._stdout = 'Generic output'

    @property
    def stdout(self):
        return self._stdout.encode('utf-8')

def test_something():
  with patch('subprocess.run', new=Mock(wraps=MockCompletedProcess)) as run_interceptor:
       # to testing with 'subprocess.run' mocked ...

Iteration 2

Python Mocking with unitest.mock 2