Python Mocking with unitest.mock: Difference between revisions
(44 intermediate revisions by the same user not shown) | |||
Line 2: | Line 2: | ||
* [[Python_Module_unittest#unittest.mock|unittest Module]] | * [[Python_Module_unittest#unittest.mock|unittest Module]] | ||
=Overview= | =Overview= | ||
The <code>unittest.mock</code> library allows replacing parts of the system under test with mock objects and make assertion about how they are accessed and used. | |||
=<tt>Mock</tt>= | |||
{{External|https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock}} | |||
=Mocking= | By default, a <code>Mock</code> accept any invocations into it. | ||
=<tt>patch() and @patch</tt>= | |||
<code>patch</code> can be used as a function decorator, class decorator or context manager. Inside the body of the decorated function or the <code>with</code> statement, first argument ("target") is patched with the instance provided as the "new" argument. When the function/with statement exits, the patch is undone. | |||
<syntaxhighlight lang='py'> | |||
def patch(target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, *, unsafe=False, **kwargs): | |||
... | |||
</syntaxhighlight> | |||
"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 <code>patch</code> 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 <code>patch</code> 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: | |||
<syntaxhighlight lang='py'> | |||
@patch("aws_helper.boto3.session") | |||
def _inject_mock_sessions(mock_target=None): | |||
mock_target.Session = Mock() | |||
... | |||
</syntaxhighlight> | |||
<font color=darkkhaki>What is "aws_helper.boto3.session" in this situation? What does represent it, as a target? It is a package. If yes, what does <code>mock_boto3_session.Session = Mock()</code> do? | |||
=Sentinel= | |||
=TODEPLETE= | |||
https://realpython.com/python-mock-library/ | |||
==Mocking== | |||
To mock a class and a method of that class: | To mock a class and a method of that class: | ||
Line 34: | Line 68: | ||
</font> | </font> | ||
==Mocking a | ===Mocking a Method=== | ||
<syntaxhighlight lang='py'> | <syntaxhighlight lang='py'> | ||
from unittest.mock import patch | from unittest.mock import patch | ||
class A: | 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' | |||
</syntaxhighlight> | |||
====Mocking an Exception==== | |||
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 | Simulating Throwing an Exception Irrespective of the Arguments it was Called With}} | |||
===Mocking a Regular Module Function=== | |||
Use <code>patch()</code> as a context manager. Inside the <code>with</code> statement, the `target` (the first argument of <code>patch()</code>) is patched with a `new` object. When the <code>with</code> 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. | |||
<syntaxhighlight lang='py'> | |||
import subprocess | |||
from unittest.mock import patch, Mock | |||
class MockCompletedProcess: | |||
@property | @property | ||
def | def stdout(self): | ||
return " | return "mock stdout" | ||
with patch(' | 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' | |||
assert | 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 | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Alternative setting where we intercept the arguments and control output depending on arguments: | |||
<syntaxhighlight lang='py'> | <syntaxhighlight lang='py'> | ||
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 | def stdout(self): | ||
return | return self._stdout.encode('utf-8') | ||
with patch | def test_something(): | ||
with patch('subprocess.run', new=Mock(wraps=MockCompletedProcess)) as run_interceptor: | |||
# to testing with 'subprocess.run' mocked ... | |||
</syntaxhighlight> | </syntaxhighlight> | ||
=Iteration 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
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:
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 ...