Python Mocking with unitest.mock

From NovaOrdis Knowledge Base
Jump to navigation Jump to search

External

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.

MagicMock

Difference between Mock and MagicMock.

patch() and @patch

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 Property

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

Tested to work:

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'

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

Asserting Invocations on Mock

mock.assert_called_once_with(
  "some concrete arg 1", 
  unittest.mock.ANY)

TO further document: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.ANY


mock = ...
mock.call_count # return the number of times the mock was called into, as an int

call_args_list

mock = ...
mock.call_args_list

call_args_list is a list of call arguments for all calls that were made on the mock. The list length is equal with mock.call_count

Call arguments for each call can be obtained with the index operator []:

args_for_first_call = mock.call_args_list[0]
args_for_second_call = mock.call_args_list[1]
...

Positional arguments are maintained in a tuple and can be obtained with the args property. It returns a tuple, which may be empty:

mock.call_args_list[0].args

Named arguments are maintained in a dictionary, which can be obtained with the kwargs property. It returns a dictionary, which may be empty:

mock.call_args_list[0].kwargs
mock.call_args_list[0].kwargs['some_arg']