>>> На главную <<<

Mock-тесты

unittest.mock

[unittest] mock предоставляет базовый Mock class. После создания объекта, можно ассертить методы и аттрибуты класса, а так-же возвращать значения и устанавливать атрибуты, если это нужно.

Дополнительно предоставляется patch() декоратор который позволяет мокать тестируемые модули и атрибуты уровня класса. MagicMock - сабкласс Mock, который предоставляет множество магических методов.

from unittest.mock import MagicMock
thing = ProductionClass()
thing.method = MagicMock(return_value=3)
thing.method(3, 4, 5, key='value')

thing.method.assert_called_with(3, 4, 5, key='value')
mock = Mock(side_effect=KeyError('foo'))
mock()

>>> Traceback (most recent call last):
 ...
KeyError: 'foo'
values = {'a': 1, 'b': 2, 'c': 3}
def side_effect(arg):
    return values[arg]

mock.side_effect = side_effect
mock('a'), mock('b'), mock('c')

mock.side_effect = [5, 4, 3, 2, 1]
mock(), mock(), mock()
from unittest.mock import patch
@patch('module.ClassName2')
@patch('module.ClassName1')
def test(MockClass1, MockClass2):
    module.ClassName1()
    module.ClassName2()
    assert MockClass1 is module.ClassName1
    assert MockClass2 is module.ClassName2
    assert MockClass1.called
    assert MockClass2.called

test()
with patch.object(ProductionClass, 'method', return_value=None) as mock_method:
    thing = ProductionClass()
    thing.method(1, 2, 3)

mock_method.assert_called_once_with(1, 2, 3)

Оригинальное состояние мокируемого объекта будет возвращено, когда тест завершается

foo = {'key': 'value'}
original = foo.copy()
with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
    assert foo == {'newkey': 'newvalue'}

assert foo == original

MagicMock мокирует все маг.методы #python

mock = MagicMock()
mock.__str__.return_value = 'foobarbaz'
str(mock)

>>> 'foobarbaz'

mock.__str__.assert_called_with()

Маг.методам можно присваивать функции

mock = Mock()
mock.__str__ = Mock(return_value='wheeeeee')
str(mock)

>>> 'wheeeeee'

Autospeck позволяет автоматически создавать моки всех атрибутов и методов функции

from unittest.mock import create_autospec
def function(a, b, c):
    pass

mock_function = create_autospec(function, return_value='fishy')
mock_function(1, 2, 3)

'fishy'

mock_function.assert_called_once_with(1, 2, 3)
mock_function('wrong arguments')

"""Traceback (most recent call last):
 ...
TypeError: <lambda>() takes exactly 3 arguments (1 given)"""

MocK class

Mock создает вызываемый объект, который создает новые моки атрибутов, когда мы получаем к ним доступ. Атрибуты всегда возвращают одно и тоже значение моков.

class unittest.mock.Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, unsafe=False, **kwargs)

Доступны такие ассерты:

mock = Mock()
mock.method(1, 2, 3, test='wow')
<Mock name='mock.method()' id='...'>
mock.method.assert_called_with(1, 2, 3, test='wow')
mock = Mock(return_value=None)
mock(1)
mock(2)
mock(3)
mock(4)
calls = [call(2), call(3)]
mock.assert_has_calls(calls)
calls = [call(4), call(2), call(3)]
mock.assert_has_calls(calls, any_order=True)

Остальные методы, начиная с reset_mock() - различные методы, конфигурирующие моки.

Доступны асинхронные моки. [asyncio]

Вызов мока

Объект мока вызываемы и возвращает значение, указанное в return_value. Дефолтно мок возвращает новый мок-объект. Возвращаемый объект создается первый раз при первом ассерте, дальше возвращается все время одно и тоже.

Вызовы пишутся в атрибуты call_args and call_args_list. Если задан side_effect, он запускается после создание объекта мока, так что вызов все равно будет записан в атрибут.

m = MagicMock(side_effect=IndexError)
m(1, 2, 3)
Traceback (most recent call last):
  ...
IndexError
m.mock_calls
[call(1, 2, 3)]
m.side_effect = KeyError('Bang!')
m('two', 'three', 'four')
Traceback (most recent call last):
  ...
KeyError: 'Bang!'
m.mock_calls
[call(1, 2, 3), call('two', 'three', 'four')]

Если использовать функцию в качестве объекта side_effect, то она принимает аргументы вызова - это позволяет поднимать ошибку или возвращать значения в зависимости от атрибутов мока

def side_effect(value):
    return value + 1

m = MagicMock(side_effect=side_effect)
m(1)
2
m(2)
3
m.mock_calls
[call(1), call(2)]

Мы так-же в любой момент можем переопределить сайд эффект и возвращать дефолтно. Два способа как это сделать:

m = MagicMock()
def side_effect(*args, **kwargs):
    return m.return_value

m.side_effect = side_effect
m.return_value = 3
m()
3
def side_effect(*args, **kwargs):
    return DEFAULT

m.side_effect = side_effect
m()
3

Чтобы возвращать дефолтное состояние, нужно установить side_effect в None

m = MagicMock(return_value=6)
def side_effect(*args, **kwargs):
    return 3

m.side_effect = side_effect
m()
3
m.side_effect = None
m()
6

Крмое того, side_effect можно итерирровать

m = MagicMock(side_effect=[1, 2, 3])
m()
1
m()
2
m()
3
m()
Traceback (most recent call last):
  ...
StopIteration

И поднимать эксепшены в процессе итерации

iterable = (33, ValueError, 66)
m = MagicMock(side_effect=iterable)
m()
33
m()
Traceback (most recent call last):
 ...
ValueError
m()
66

Удаление атрибутов

mock = MagicMock()
hasattr(mock, 'm')
True
del mock.m
hasattr(mock, 'm')
False
del mock.f
mock.f
Traceback (most recent call last):
    ...
AttributeError: f

Название mocka (атрибут name)

Использование мока, как атрибута

При присоединении мока, как атрибута к другому моку, он становится дочерним моком.

parent = MagicMock()
child1 = MagicMock(return_value=None)
child2 = MagicMock(return_value=None)
parent.child1 = child1
parent.child2 = child2
child1(1)
child2(2)
parent.mock_calls
[call.child1(1), call.child2(2)]

Другие подробности в статье

The patchers

Декораторы patch используются для исправления объектов только в рамках функции, которую они декорируют. Они автоматически обрабатывают распаковку, даже если возникают исключения. Все эти функции также могут использоваться в операторах with или в качестве декораторов классов.

unittest.mock.patch(target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, **kwargs)

Может использоваться как декоратор функции, класса или контекст-менеджер.

Подробнее об остальном в статье

>>> @patch('__main__.SomeClass')
... def function(normal_argument, mock_class):
...     print(mock_class is SomeClass)
...
>>> function(None)
True
>>> class Class:
...     def method(self):
...         pass
...
>>> with patch('__main__.Class') as MockClass:
...     instance = MockClass.return_value
...     instance.method.return_value = 'foo'
...     assert Class() is instance
...     assert Class().method() == 'foo'

Пачт может получать различные объекты

@patch.object(SomeClass, 'class_method')
def test(mock_method):
    SomeClass.class_method(3)
    mock_method.assert_called_with(3)

test()
foo = {}
@patch.dict(foo, {'newkey': 'newvalue'})
def test():
    assert foo == {'newkey': 'newvalue'}
test()
assert foo == {}
with patch.multiple(settings, FIRST_PATCH='one', SECOND_PATCH='two'):
    ...

У патчей есть start() и stop() методы. Это упрощает setUp для теста

patcher = patch('package.module.ClassName')
from package import module
original = module.ClassName
new_mock = patcher.start()
assert module.ClassName is not original
assert module.ClassName is new_mock
patcher.stop()
assert module.ClassName is original
assert module.ClassName is not new_mock

Пример с сетап-тирдаун

class MyTest(unittest.TestCase):
    def setUp(self):
        self.patcher1 = patch('package.module.Class1')
        self.patcher2 = patch('package.module.Class2')
        self.MockClass1 = self.patcher1.start()
        self.MockClass2 = self.patcher2.start()

    def tearDown(self):
        self.patcher1.stop()
        self.patcher2.stop()

    def test_something(self):
        assert package.module.Class1 is self.MockClass1
        assert package.module.Class2 is self.MockClass2

MyTest('test_something').run()

Важно! Если setup вызовет ошибку - tearDown’а уже не будет. Проблему помогает решить unittest.TestCase.addCleanup()

class MyTest(unittest.TestCase):
    def setUp(self):
        patcher = patch('package.module.Class')
        self.MockClass = patcher.start()
        self.addCleanup(patcher.stop)

    def test_something(self):
        assert package.module.Class is self.MockClass

Еще доступно вот это: patch.stopall()

Test prefix

Это позволяет передать через декоратор что-то только специфическим методам класса, если декорируется класс. Аналог unittest.TestLoader

>>> patch.TEST_PREFIX = 'foo'
>>> value = 3
>>>
>>> @patch('__main__.value', 'not three')
... class Thing:
...     def foo_one(self):
...         print(value)
...     def foo_two(self):
...         print(value)
...
>>>
>>> Thing().foo_one()
not three
>>> Thing().foo_two()
not three
>>> value
3

Декораторы patch можно стакать

@patch.object(SomeClass, 'class_method')
@patch.object(SomeClass, 'static_method')
def test(mock1, mock2):
    assert SomeClass.static_method is mock1
    assert SomeClass.class_method is mock2
    SomeClass.static_method('foo')
    SomeClass.class_method('bar')
    return mock1, mock2

mock1, mock2 = test()
mock1.assert_called_once_with('foo')
mock2.assert_called_once_with('bar')

Остальное смотир в доке

MagicMock

Позволяет мокироват ьмагические методы #python. Если для мока используется функция, ей обязательно необходимо передать self

def __str__(self):
    return 'fooble'
mock = Mock()
mock.__str__ = __str__
str(mock)


mock = Mock()
mock.__str__ = Mock()
mock.__str__.return_value = 'fooble'
str(mock)
'fooble'


mock = Mock()
mock.__iter__ = Mock(return_value=iter([]))
list(mock)
[]

Вот так можно замокать контекст-менеджер:

mock = Mock()
mock.__enter__ = Mock(return_value='foo')
mock.__exit__ = Mock(return_value=False)
with mock as m:
    assert m == 'foo'

mock.__enter__.assert_called_with()
mock.__exit__.assert_called_with(None, None, None)

Полный список того, что можно замокать:

Данные методы не поддерживаются либо могут вызывать проблемы:

MagicMock

Два варианта MagicMock and NonCallableMagicMock. Первый - это сабкласс от Mock, реализующий большинство меджик методов. Второй - его невызываемый собрат. Его конструктор аналогичен первому за исключением того, что return_value и side_effect не имеют значения.

mock = MagicMock()
mock[3] = 'fish'
mock.__setitem__.assert_called_with(3, 'fish')
mock.__getitem__.return_value = 'result'
mock[2]
'result'

Доступные методы и их дефолтные значения

mock = MagicMock()
int(mock)
1
len(mock)
0
list(mock)
[]
object() in mock
False

Подробнее читай доку - есть нюансы. Так-же ест ьметоды, котоыре поддерживаются, но не определены дефолтные значения.

Helpers

unittest.mock.sentinel простйо способ создавать уникальные объекты для тестов unittest.mock.DEFAULT заранее созданный сентинел. Как используется - смотри выше, там где обсуждался вывод дефолтного значения в side_effect unittest.mock.call(*args, **kwargs) хелпер для упрощения ассершена

m = MagicMock(return_value=None)
m(1, 2, a='foo', b='bar')
m()
m.call_args_list == [call(1, 2, a='foo', b='bar'), call()]
True

Реализованы методы call_args, call_args_list, mock_calls и method_calls. Подробнее

mock = Mock(return_value=None)
mock('foo', bar=object())
mock.assert_called_once_with('foo', bar=ANY)
m = MagicMock(return_value=None)
m(1)
m(1, 2)
m(object())
m.mock_calls == [call(1), call(1, 2), ANY]
True

Monkeypatching/mocking modules and environments

Моки модулей и окружений можно делать в [pytest]. The monkeypatch fixture helps you to safely set/delete an attribute, dictionary item or environment variable, or to modify sys.path for importing

monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.setitem(mapping, name, value)
monkeypatch.delitem(obj, name, raising=True)
monkeypatch.setenv(name, value, prepend=False)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)

Все модификации удаляются после того, как запрашиваемая функция в фикстуре выполняется. raising параметр определяет, что происходит при KeyError или AttributeError

Когда надо использовать?

Простой пример

# contents of test_module.py with source code and the test
from pathlib import Path


def getssh():
    """Simple function to return expanded homedir ssh path."""
    return Path.home() / ".ssh"


def test_getssh(monkeypatch):
    # mocked return function to replace Path.home
    # always return '/abc'
    def mockreturn():
        return Path("/abc")

    # Application of the monkeypatch to replace Path.home
    # with the behavior of mockreturn defined above.
    monkeypatch.setattr(Path, "home", mockreturn)

    # Calling getssh() will use mockreturn in place of Path.home
    # for this test with the monkeypatch.
    x = getssh()
    assert x == Path("/abc/.ssh")

Можно мокать возвращаемый объект, для этого надо создать мок-класс

# contents of app.py, a simple API retrieval example
import requests


def get_json(url):
    """Takes a URL, and returns the JSON."""
    r = requests.get(url)
    return r.json()
# contents of test_app.py, a simple test for our API retrieval
# import requests for the purposes of monkeypatching
import requests

# our app.py that includes the get_json() function
# this is the previous code block example
import app

# custom class to be the mock return value
# will override the requests.Response returned from requests.get
class MockResponse:

    # mock json() method always returns a specific testing dictionary
    @staticmethod
    def json():
        return {"mock_key": "mock_response"}


def test_get_json(monkeypatch):

    # Any arguments may be passed and mock_get() will always return our
    # mocked object, which only has the .json() method.
    def mock_get(*args, **kwargs):
        return MockResponse()

    # apply the monkeypatch for requests.get to mock_get
    monkeypatch.setattr(requests, "get", mock_get)

    # app.get_json, which contains requests.get, uses the monkeypatch
    result = app.get_json("https://fakeurl")
    assert result["mock_key"] == "mock_response"

Как мокать другие объекты - читай в документации

Либа pytest-mock предоставляет доп.апи для моков в pytest, напирмер позволяет работать с файловой системой

import os

class UnixFS:

    @staticmethod
    def rm(filename):
        os.remove(filename)

def test_unix_fs(mocker):
    mocker.patch('os.remove')
    UnixFS.rm('file')
    os.remove.assert_called_once_with('file')

>>> На главную <<<