Основы библиотеки Python Mock Object

Оглавление

Когда вы пишете надежный код, тесты необходимы для проверки правильности, надежности и эффективности логики вашего приложения. Однако ценность ваших тестов зависит от того, насколько хорошо они демонстрируют эти критерии. Такие препятствия, как сложная логика и непредсказуемые зависимости, затрудняют написание ценных тестов. Библиотека Python mock-объектов unittest.mock поможет вам преодолеть эти препятствия.

К концу этой статьи вы сможете:

  • Создайте Python mock-объекты с помощью Mock
  • Убедитесь, что вы используете объекты по назначению
  • Проверьте данные об использовании, хранящиеся на ваших Python-макетах
  • Настройте определенные аспекты ваших Python-макетов
  • Замените ваши имитаторы реальными объектами с помощью patch()
  • Избегайте распространенных проблем, присущих Python mocking

Вначале вы узнаете, что такое мокинг и как он улучшает ваши тесты.

Что такое Mock?

Макетный объект заменяет и имитирует реальный объект в среде тестирования. Это универсальный и мощный инструмент для улучшения качества ваших тестов.

Одной из причин использования имитационных объектов Python является контроль поведения вашего кода во время тестирования.

Например, если ваш код выполняет HTTP-запросы к внешним сервисам, то ваши тесты выполняются предсказуемо только до тех пор, пока эти сервисы ведут себя так, как вы ожидали. Иногда временное изменение в поведении этих внешних сервисов может вызвать периодические сбои в вашем тестовом наборе.

Поэтому лучше тестировать код в контролируемой среде. Замена реального запроса объектом-макетом позволит вам имитировать сбои в работе внешних сервисов и успешные ответы предсказуемым образом.

Иногда бывает трудно протестировать определенные области кодовой базы. К таким областям относятся блоки except и утверждения if, которые трудно удовлетворить. Использование имитационных объектов Python поможет вам контролировать путь выполнения вашего кода, чтобы достичь этих областей и улучшить покрытие кода.

Еще одна причина использовать объекты-макеты - лучше понять, как вы используете их реальные аналоги в своем коде. Макетный объект Python содержит данные о его использовании, которые вы можете проверить, например:

  • Если вы вызвали метод
  • Как вы вызывали метод
  • Как часто вы вызывали метод

Понимание того, что делает объект-макет, - это первый шаг к тому, чтобы научиться его использовать.

Теперь вы увидите, как использовать Python mock-объекты.

Библиотека Python Mock

Библиотека Python mock object library - это unittest.mock. Она обеспечивает простой способ внедрения моков в ваши тесты.

Примечание: Стандартная библиотека включает unittest.mock в Python 3.3 и более поздних версиях. Если вы используете более старую версию Python, вам нужно установить официальный бэкпорт библиотеки. Для этого установите mock из PyPI:

$ pip install mock

unittest.mock предоставляет класс Mock, который вы будете использовать для имитации реальных объектов в своей кодовой базе. Mock предлагает невероятную гибкость и глубокие данные. Этот класс, а также его подклассы удовлетворят большинство потребностей в имитации Python, с которыми вы столкнетесь в своих тестах.

Библиотека также предоставляет функцию patch(), которая заменяет реальные объекты в вашем коде на экземпляры Mock. Вы можете использовать patch() как декоратор или менеджер контекста, предоставляя вам контроль над областью, в которой объект будет имитироваться. Как только указанная область выйдет, patch() очистит ваш код, заменив сымитированные объекты их оригинальными аналогами.

Наконец, unittest.mock предлагает решения некоторых проблем, присущих объектам mocking.

Теперь вы лучше понимаете, что такое mocking и какую библиотеку вы будете использовать для этого. Давайте погрузимся внутрь и изучим, какие возможности и функциональность предлагает unittest.mock.

Объект Mock

unittest.mock предлагает базовый класс для подражания объектам под названием Mock. Варианты использования Mock практически безграничны, поскольку Mock настолько гибок.

Начните с инстанцирования нового экземпляра Mock:

>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock
<Mock id='4561344720'>

Теперь вы можете заменить объект в своем коде на новый Mock. Вы можете сделать это, передав его в качестве аргумента функции или переопределив другой объект:

# Pass mock as an argument to do_something()
do_something(mock)

# Patch the json library
json = mock

Когда вы подставляете объект в код, Mock должен выглядеть как настоящий объект, который он заменяет. В противном случае ваш код не сможет использовать Mock вместо оригинального объекта.

Например, если вы подражаете библиотеке json и ваша программа вызывает dumps(), то ваш объект подражания Python должен также содержать dumps().

Далее вы увидите, как Mock справляется с этой проблемой.

Ленивые атрибуты и методы

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

>>> mock.some_attribute
<Mock name='mock.some_attribute' id='4394778696'>
>>> mock.do_something()
<Mock name='mock.do_something()' id='4394778920'>

Поскольку Mock может создавать произвольные атрибуты на лету, он подходит для замены любого объекта.

Используя пример, приведенный ранее, если вы подражаете библиотеке json и вызываете dumps(), объект подражания Python создаст метод так, чтобы его интерфейс соответствовал интерфейсу библиотеки:

>>> json = Mock()
>>> json.dumps()
<Mock name='mock.dumps()' id='4392249776'>

Обратите внимание на две ключевые характеристики этой издевательской версии dumps():

  1. В отличие от real dumps(), этот подражаемый метод не требует аргументов. Фактически, он будет принимать любые аргументы, которые вы ему передадите.

  2. Возвращаемое значение dumps() также является Mock. Возможность Mock рекурсивно определять другие мейки позволяет использовать мейки в сложных ситуациях:

>>> json = Mock()
>>> json.loads('{"k": "v"}').get('k')
<Mock name='mock.loads().get()' id='4379599424'>

Поскольку возвращаемое значение каждого имитируемого метода также является Mock, вы можете использовать свои имитаторы множеством способов.

Моки отличаются гибкостью, но они также информативны. Далее вы узнаете, как можно использовать моки, чтобы лучше понимать свой код.

Утверждения и проверка

Экземпляры

Mock хранят данные о том, как вы их использовали. Например, вы можете узнать, вызывали ли вы метод, как вы его вызывали и так далее. Существует два основных способа использования этой информации.

Во-первых, вы можете утверждать, что ваша программа использовала объект так, как вы ожидали:

>>> from unittest.mock import Mock

>>> # Create a mock object
... json = Mock()

>>> json.loads('{"key": "value"}')
<Mock name='mock.loads()' id='4550144184'>

>>> # You know that you called loads() so you can
>>> # make assertions to test that expectation
... json.loads.assert_called()
>>> json.loads.assert_called_once()
>>> json.loads.assert_called_with('{"key": "value"}')
>>> json.loads.assert_called_once_with('{"key": "value"}')

>>> json.loads('{"key": "value"}')
<Mock name='mock.loads()' id='4550144184'>

>>> # If an assertion fails, the mock will raise an AssertionError
... json.loads.assert_called_once()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 795, in assert_called_once
    raise AssertionError(msg)
AssertionError: Expected 'loads' to have been called once. Called 2 times.

>>> json.loads.assert_called_once_with('{"key": "value"}')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 824, in assert_called_once_with
    raise AssertionError(msg)
AssertionError: Expected 'loads' to be called once. Called 2 times.

>>> json.loads.assert_not_called()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 777, in assert_not_called
    raise AssertionError(msg)
AssertionError: Expected 'loads' to not have been called. Called 2 times.

.assert_called() гарантирует, что вы вызвали подражаемый метод, а .assert_called_once() проверяет, что вы вызвали метод ровно один раз.

У обеих функций утверждения есть варианты, позволяющие проверять аргументы, передаваемые имитируемому методу:

  • .assert_called_with(*args, **kwargs)
  • .assert_called_once_with(*args, **kwargs)

Чтобы передать эти утверждения, вы должны вызвать имитируемый метод с теми же аргументами, которые вы передаете реальному методу:

>>> json = Mock()
>>> json.loads(s='{"key": "value"}')
>>> json.loads.assert_called_with('{"key": "value"}')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 814, in assert_called_with
    raise AssertionError(_error_message()) from cause
AssertionError: Expected call: loads('{"key": "value"}')
Actual call: loads(s='{"key": "value"}')
>>> json.loads.assert_called_with(s='{"key": "value"}')

json.loads.assert_called_with('{"key": "value"}') вызвал ошибку AssertionError, потому что ожидал, что вы вызовете loads() с позиционным аргументом, а на самом деле вы вызвали его с аргументом ключевого слова. json.loads.assert_called_with(s='{"key": "value"}') делает это утверждение правильным.

Во-вторых, вы можете просмотреть специальные атрибуты, чтобы понять, как ваше приложение использовало объект:

>>> from unittest.mock import Mock

>>> # Create a mock object
... json = Mock()
>>> json.loads('{"key": "value"}')
<Mock name='mock.loads()' id='4391026640'>

>>> # Number of times you called loads():
... json.loads.call_count
1
>>> # The last loads() call:
... json.loads.call_args
call('{"key": "value"}')
>>> # List of loads() calls:
... json.loads.call_args_list
[call('{"key": "value"}')]
>>> # List of calls to json's methods (recursively):
... json.method_calls
[call.loads('{"key": "value"}')]

Вы можете писать тесты, используя эти атрибуты, чтобы убедиться, что ваши объекты ведут себя так, как вы задумали.

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

Управление возвращаемым значением макета

Одной из причин использования макетов является контроль поведения кода во время тестирования. Один из способов сделать это - указать возвращаемое значение функции . Давайте на примере посмотрим, как это работает.

Сначала создайте файл с именем my_calendar.py. Добавьте is_weekday(), функцию, которая использует библиотеку datetime Python для определения того, является ли сегодняшний день днем недели. Наконец, напишите тест, утверждающий, что функция работает так, как ожидалось:

from datetime import datetime

def is_weekday():
    today = datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return (0 <= today.weekday() < 5)

# Test if today is a weekday
assert is_weekday()

Поскольку вы проверяете, является ли сегодняшний день будним, результат зависит от дня проведения теста:

$ python my_calendar.py

Если эта команда не выдает никаких результатов, значит, утверждение прошло успешно. К сожалению, если вы запустите команду в выходной день, вы получите сообщение AssertionError:

$ python my_calendar.py
Traceback (most recent call last):
  File "test.py", line 9, in <module>
    assert is_weekday()
AssertionError

При написании тестов важно обеспечить предсказуемость результатов. Вы можете использовать Mock, чтобы исключить неопределенность из кода во время тестирования. В этом случае вы можете поиздеваться над datetime и установить .return_value для .today() день, который вы выберете сами:

import datetime
from unittest.mock import Mock

# Save a couple of test days
tuesday = datetime.datetime(year=2019, month=1, day=1)
saturday = datetime.datetime(year=2019, month=1, day=5)

# Mock datetime to control today's date
datetime = Mock()

def is_weekday():
    today = datetime.datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return (0 <= today.weekday() < 5)

# Mock .today() to return Tuesday
datetime.datetime.today.return_value = tuesday
# Test Tuesday is a weekday
assert is_weekday()
# Mock .today() to return Saturday
datetime.datetime.today.return_value = saturday
# Test Saturday is not a weekday
assert not is_weekday()

В примере .today() - это подражаемый метод. Вы устранили несоответствие, назначив определенный день для .return_value в макете. Таким образом, при вызове .today() он возвращает тот datetime, который вы указали.

В первом тесте вы убеждаетесь, что tuesday - будний день. Во втором тесте вы проверяете, что saturday не является будним днем. Теперь неважно, в какой день вы запускаете тесты, потому что вы высмеяли datetime и контролируете поведение объекта.

Дальнейшее чтение: Несмотря на то, что подобная имитация datetime является хорошим практическим примером для использования Mock, уже существует фантастическая библиотека для имитации datetime под названием freezegun.

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

Иногда требуется, чтобы функции возвращали разные значения при неоднократном обращении к ним или даже вызывали исключения. Это можно сделать с помощью .side_effect.

Управление побочными эффектами макета

Вы можете управлять поведением вашего кода, указывая побочные эффекты в подражаемой функции. Побочные эффекты .side_effect определяют, что происходит при вызове имитируемой функции.

Чтобы проверить, как это работает, добавьте новую функцию в my_calendar.py:

import requests

def get_holidays():
    r = requests.get('http://localhost/api/holidays')
    if r.status_code == 200:
        return r.json()
    return None

get_holidays() делает запрос к серверу localhost на набор праздников. Если сервер ответит успешно, get_holidays() вернет словарь. В противном случае метод вернет None.

Вы можете проверить, как get_holidays() будет реагировать на таймаут соединения, установив requests.get.side_effect.

В этом примере вы увидите только соответствующий код из my_calendar.py. Вы создадите тестовый пример, используя библиотеку Python unittest:

import unittest
from requests.exceptions import Timeout
from unittest.mock import Mock

# Mock requests to control its behavior
requests = Mock()

def get_holidays():
    r = requests.get('http://localhost/api/holidays')
    if r.status_code == 200:
        return r.json()
    return None

class TestCalendar(unittest.TestCase):
    def test_get_holidays_timeout(self):
        # Test a connection timeout
        requests.get.side_effect = Timeout
        with self.assertRaises(Timeout):
            get_holidays()

if __name__ == '__main__':
    unittest.main()

Вы используете .assertRaises(), чтобы проверить, что get_holidays() вызывает исключение, учитывая новый побочный эффект get().

Запустите этот тест, чтобы увидеть результат вашей проверки:

$ python my_calendar.py
.
-------------------------------------------------------
Ran 1 test in 0.000s

OK

Если вы хотите быть немного более динамичным, вы можете установить .side_effect в функцию, которая Mock будет вызываться при вызове вашего mocked-метода. Макет разделяет аргументы и возвращаемое значение функции .side_effect:

import requests
import unittest
from unittest.mock import Mock

# Mock requests to control its behavior
requests = Mock()

def get_holidays():
    r = requests.get('http://localhost/api/holidays')
    if r.status_code == 200:
        return r.json()
    return None

class TestCalendar(unittest.TestCase):
    def log_request(self, url):
        # Log a fake request for test output purposes
        print(f'Making a request to {url}.')
        print('Request received!')

        # Create a new Mock to imitate a Response
        response_mock = Mock()
        response_mock.status_code = 200
        response_mock.json.return_value = {
            '12/25': 'Christmas',
            '7/4': 'Independence Day',
        }
        return response_mock

    def test_get_holidays_logging(self):
        # Test a successful, logged request
        requests.get.side_effect = self.log_request
        assert get_holidays()['12/25'] == 'Christmas'

if __name__ == '__main__':
    unittest.main()

Сначала вы создали .log_request(), который принимает URL, регистрирует некоторые выходные данные с помощью print(), а затем возвращает ответ Mock. Далее вы устанавливаете .side_effect из get() в .log_request(), который вы будете использовать при вызове get_holidays(). Запустив тест, вы увидите, что get() передает свои аргументы в .log_request(), затем принимает возвращаемое значение и также возвращает его:

$ python my_calendar.py
Making a request to http://localhost/api/holidays.
Request received!
.
-------------------------------------------------------
Ran 1 test in 0.000s

OK

Отлично! Операторы print() регистрируют правильные значения. Кроме того, get_holidays() вернул словарь праздников.

.side_effect также может быть итерируемым. Итерабель должен состоять из возвращаемых значений, исключений или смеси того и другого. Итерабель будет выдавать свое следующее значение каждый раз, когда вы вызываете свой имитируемый метод. Например, вы можете проверить, что повторная попытка после Timeout возвращает успешный ответ:

import unittest
from requests.exceptions import Timeout
from unittest.mock import Mock

# Mock requests to control its behavior
requests = Mock()

def get_holidays():
    r = requests.get('http://localhost/api/holidays')
    if r.status_code == 200:
        return r.json()
    return None

class TestCalendar(unittest.TestCase):
    def test_get_holidays_retry(self):
        # Create a new Mock to imitate a Response
        response_mock = Mock()
        response_mock.status_code = 200
        response_mock.json.return_value = {
            '12/25': 'Christmas',
            '7/4': 'Independence Day',
        }
        # Set the side effect of .get()
        requests.get.side_effect = [Timeout, response_mock]
        # Test that the first request raises a Timeout
        with self.assertRaises(Timeout):
            get_holidays()
        # Now retry, expecting a successful response
        assert get_holidays()['12/25'] == 'Christmas'
        # Finally, assert .get() was called twice
        assert requests.get.call_count == 2

if __name__ == '__main__':
    unittest.main()

При первом вызове get_holidays() метод get() вызывает ошибку Timeout. Во второй раз метод возвращает правильный словарь праздников. Эти побочные эффекты соответствуют порядку их появления в списке, переданном в .side_effect.

Вы можете установить .return_value и .side_effect непосредственно на Mock. Однако, поскольку объект Python mock должен быть гибким в создании своих атрибутов, существует лучший способ настройки этих и других параметров.

Настройка вашего макета

Вы можете сконфигурировать Mock, чтобы задать некоторые поведенческие характеристики объекта. К числу конфигурируемых членов относятся .side_effect, .return_value и .name. Вы настраиваете Mock, когда создаете или когда используете .configure_mock().

Вы можете настроить Mock, указав определенные атрибуты при инициализации объекта:

>>> mock = Mock(side_effect=Exception)
>>> mock()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 939, in __call__
    return _mock_self._mock_call(*args, **kwargs)
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 995, in _mock_call
    raise effect
Exception

>>> mock = Mock(name='Real Python Mock')
>>> mock
<Mock name='Real Python Mock' id='4434041432'>

>>> mock = Mock(return_value=True)
>>> mock()
True

В то время как .side_effect и .return_value могут быть установлены на самом экземпляре Mock, другие атрибуты, такие как .name, могут быть установлены только через .__init__() или .configure_mock(). Если вы попытаетесь установить .name из Mock на экземпляре, вы получите другой результат:

>>> mock = Mock(name='Real Python Mock')
>>> mock.name
<Mock name='Real Python Mock.name' id='4434041544'>

>>> mock = Mock()
>>> mock.name = 'Real Python Mock'
>>> mock.name
'Real Python Mock'

.name - это обычный атрибут, используемый объектами. Таким образом, Mock не позволяет установить это значение для экземпляра так же, как это можно сделать с помощью .return_value или .side_effect. Если вы обратитесь к mock.name, вы создадите атрибут .name вместо того, чтобы настраивать свой макет.

Вы можете настроить существующий Mock с помощью .configure_mock():

>>> mock = Mock()
>>> mock.configure_mock(return_value=True)
>>> mock()
True

Распаковывая словарь в .configure_mock() или Mock.__init__(), вы можете даже настроить атрибуты объекта Python mock. Используя конфигурации Mock, вы можете упростить предыдущий пример:

# Verbose, old Mock
response_mock = Mock()
response_mock.json.return_value = {
    '12/25': 'Christmas',
    '7/4': 'Independence Day',
}

# Shiny, new .configure_mock()
holidays = {'12/25': 'Christmas', '7/4': 'Independence Day'}
response_mock = Mock(**{'json.return_value': holidays})

Теперь вы можете создавать и настраивать объекты Python mock. Вы также можете использовать имитаторы для управления поведением вашего приложения. До сих пор вы использовали mock-объекты в качестве аргументов функций или объектов исправления в том же модуле, что и ваши тесты.

Далее вы узнаете, как заменять имитаторы реальными объектами в других модулях.

patch()

unittest.mock предоставляет мощный механизм для подражания объектам, называемый patch(), который ищет объект в данном модуле и заменяет его на Mock.

Обычно вы используете patch() в качестве декоратора или менеджера контекста, чтобы предоставить область видимости, в которой вы будете высмеивать целевой объект.

patch() в роли Декоратора

Если вы хотите подражать объекту в течение всего времени работы тестовой функции, вы можете использовать patch() в качестве функции декоратора.

Чтобы увидеть, как это работает, реорганизуйте ваш my_calendar.py файл, поместив логику и тесты в отдельные файлы:

import requests
from datetime import datetime

def is_weekday():
    today = datetime.today()
    # Python's datetime library treats Monday as 0 and Sunday as 6
    return (0 <= today.weekday() < 5)

def get_holidays():
    r = requests.get('http://localhost/api/holidays')
    if r.status_code == 200:
        return r.json()
    return None

Теперь эти функции находятся в собственном файле, отдельно от их тестов. Далее вы заново создадите свои тесты в файле под названием tests.py.

До этого момента вы патчили объекты обезьянкой в файле, в котором они существуют. Обезьянье исправление - это замена одного объекта другим во время выполнения программы. Теперь вы будете использовать patch() для замены объектов в my_calendar.py:

import unittest
from my_calendar import get_holidays
from requests.exceptions import Timeout
from unittest.mock import patch

class TestCalendar(unittest.TestCase):
    @patch('my_calendar.requests')
    def test_get_holidays_timeout(self, mock_requests):
            mock_requests.get.side_effect = Timeout
            with self.assertRaises(Timeout):
                get_holidays()
                mock_requests.get.assert_called_once()

if __name__ == '__main__':
    unittest.main()

Изначально вы создали Mock и исправили requests в локальной области видимости. Теперь вам нужно получить доступ к библиотеке requests в my_calendar.py из tests.py.

Для этого случая вы использовали patch() в качестве декоратора и передали путь к целевому объекту. Целевой путь был 'my_calendar.requests', состоящий из имени модуля и объекта.

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

Вы можете выполнить этот тестовый модуль, чтобы убедиться, что он работает так, как ожидалось:

$ python tests.py
.
-------------------------------------------------------
Ran 1 test in 0.001s

OK

Техническое описание: patch() возвращает экземпляр MagicMock, который является Mock подклассом. MagicMock полезен тем, что реализует для вас большинство магических методов, таких как .__len__(), .__str__() и .__iter__(), с разумными значениями по умолчанию.

Использование

в качестве декоратора patch() хорошо сработало в этом примере. В некоторых случаях удобнее читать, эффективнее или проще использовать patch() в качестве менеджера контекста.

patch() в качестве менеджера контекста

Иногда вы захотите использовать patch() в качестве менеджера контекста, а не декоратора. Некоторые причины, по которым вы можете предпочесть менеджер контекста, включают следующее:

  • Вы хотите подражать объекту только в части тестовой области.
  • Вы уже используете слишком много декораторов или параметров, что ухудшает читаемость теста.

Чтобы использовать patch() в качестве менеджера контекста, вы используете оператор with в Python:

import unittest
from my_calendar import get_holidays
from requests.exceptions import Timeout
from unittest.mock import patch

class TestCalendar(unittest.TestCase):
    def test_get_holidays_timeout(self):
        with patch('my_calendar.requests') as mock_requests:
            mock_requests.get.side_effect = Timeout
            with self.assertRaises(Timeout):
                get_holidays()
                mock_requests.get.assert_called_once()

if __name__ == '__main__':
    unittest.main()

Когда тест выходит из оператора with, patch() заменяет подражаемый объект на оригинальный.

До сих пор вы высмеивали целые объекты, но иногда вам нужно высмеять только часть объекта.

Исправление атрибутов объекта

Допустим, вы хотите высмеять только один метод объекта, а не весь объект. Вы можете сделать это, используя patch.object().

Например, .test_get_holidays_timeout() действительно нуждается только в том, чтобы высмеять requests.get() и установить его .side_effect в Timeout:

import unittest
from my_calendar import requests, get_holidays
from unittest.mock import patch

class TestCalendar(unittest.TestCase):
    @patch.object(requests, 'get', side_effect=requests.exceptions.Timeout)
    def test_get_holidays_timeout(self, mock_requests):
            with self.assertRaises(requests.exceptions.Timeout):
                get_holidays()

if __name__ == '__main__':
    unittest.main()

В этом примере вы высмеяли только get(), а не все requests. Все остальные атрибуты остались прежними.

object() принимает те же параметры конфигурации, что и patch(). Но вместо того, чтобы передавать путь к цели, в качестве первого параметра вы указываете сам целевой объект. Второй параметр - это атрибут целевого объекта, который вы пытаетесь сымитировать. Вы также можете использовать object() в качестве менеджера контекста, как patch().

Дальнейшее чтение: Помимо объектов и атрибутов, вы также можете patch() словари с patch.dict().

Узнать, как использовать patch(), очень важно для подражания объектам в других модулях. Однако иногда путь к целевому объекту не очевиден.

Где ставить патч

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

Допустим, вы издеваетесь над is_weekday() в my_calendar.py, используя patch():

>>> import my_calendar
>>> from unittest.mock import patch

>>> with patch('my_calendar.is_weekday'):
...     my_calendar.is_weekday()
...
<MagicMock name='is_weekday()' id='4336501256'>

Сначала вы импортируете my_calendar.py. Затем вы исправляете is_weekday(), заменяя его на Mock. Отлично! Все работает, как и ожидалось.

Теперь давайте немного изменим этот пример и импортируем функцию напрямую:

>>> from my_calendar import is_weekday
>>> from unittest.mock import patch

>>> with patch('my_calendar.is_weekday'):
...     is_weekday()
...
False

Примечание: В зависимости от того, в какой день вы читаете этот учебник, вывод консоли может иметь вид True или False. Главное, чтобы в выводе не было Mock, как раньше.

Обратите внимание, что, хотя целевое местоположение, которое вы передали в patch(), не изменилось, результат вызова is_weekday() отличается. Это связано с изменением способа импорта функции.

from my_calendar import is_weekday привязывает реальную функцию к локальной области видимости. Таким образом, даже если вы patch() функцию позже, вы игнорируете имитацию, потому что у вас уже есть локальная ссылка на неимитированную функцию.

Хорошим правилом является patch() объект, на который смотрят сверху.

В первом примере издевательство над 'my_calendar.is_weekday()' работает, потому что вы ищете функцию в модуле my_calendar. Во втором примере у вас есть локальная ссылка на is_weekday(). Поскольку вы используете функцию, найденную в локальной области видимости, вам следует высмеять локальную функцию:

>>> from unittest.mock import patch
>>> from my_calendar import is_weekday

>>> with patch('__main__.is_weekday'):
...     is_weekday()
...
<MagicMock name='is_weekday()' id='4502362992'>

Теперь вы хорошо знаете возможности patch(). Вы увидели, как patch() объекты и атрибуты, а также где их подключать.

Далее вы увидите некоторые общие проблемы, присущие объектному мокингу, и решения, которые предлагает unittest.mock.

Распространенные проблемы с высмеиванием

Подражание объектам может внести несколько проблем в ваши тесты. Одни проблемы присущи мокингу, другие специфичны для unittest.mock. Имейте в виду, что существуют и другие проблемы с мокингом, которые не упоминаются в этом руководстве.

Рассмотренные здесь варианты похожи друг на друга тем, что проблема, которую они вызывают, в корне одинакова. В каждом случае тестовые утверждения не имеют значения. Хотя намерение каждого из них обосновано, сами они не являются таковыми.

Изменения в интерфейсах объектов и ошибки в написании

Классы и определения функций постоянно меняются. Когда интерфейс объекта меняется, любые тесты, опирающиеся на Mock этого объекта, могут стать неактуальными.

Например, вы переименовали метод, но забыли, что тест подражает этому методу и вызывает .assert_not_called(). После изменения .assert_not_called() по-прежнему является True. Утверждение, однако, бесполезно, поскольку метод больше не существует.

Неактуальные тесты могут показаться не критичными, но если это ваши единственные тесты и вы предполагаете, что они работают правильно, ситуация может оказаться катастрофической для вашего приложения.

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

Если вы вызовете .asert_called() вместо .assert_called(), ваш тест не вызовет AssertionError. Это происходит потому, что вы создали новый метод на объекте Python mock с именем .asert_called() вместо того, чтобы оценивать фактическое утверждение.

Технические подробности: Интересно, что assret - это специальное неправильное написание assert. Если вы попытаетесь получить доступ к атрибуту, который начинается с assret (или assert), Mock автоматически вызовет ошибку AttributeError.

Эти проблемы возникают, когда вы подражаете объектам внутри собственной кодовой базы. Другая проблема возникает, когда вы моделируете объекты, взаимодействующие с внешними кодовыми базами.

Изменения во внешних зависимостях

Снова представьте, что ваш код делает запрос к внешнему API. В данном случае внешняя зависимость - это API, который может быть изменен без вашего согласия.

С одной стороны, юнит-тесты тестируют изолированные компоненты кода. Таким образом, имитация кода, выполняющего запрос, помогает вам тестировать изолированные компоненты в контролируемых условиях. Однако это также представляет потенциальную проблему.

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

К сожалению, это не та проблема, для которой unittest.mock предлагает решение. Вы должны проявлять благоразумие при подражании внешним зависимостям.

Все эти три проблемы могут привести к неактуальности тестов и потенциально дорогостоящим проблемам, поскольку они угрожают целостности ваших имитаторов. unittest.mock предлагает вам несколько инструментов для решения этих проблем.

Избежание распространенных проблем при использовании спецификаций

Как уже говорилось, если вы измените определение класса или функции или неправильно напишете атрибут объекта Python mock, это может вызвать проблемы с вашими тестами.

Эти проблемы возникают из-за того, что Mock создает атрибуты и методы, когда вы обращаетесь к ним. Решение этих проблем заключается в том, чтобы не позволить Mock создавать атрибуты, которые не соответствуют объекту, который вы пытаетесь высмеять.

При конфигурировании Mock можно передать спецификацию объекта в параметр spec. Параметр spec принимает список имен или другой объект и определяет интерфейс имитатора. Если вы попытаетесь получить доступ к атрибуту, не входящему в спецификацию, Mock вызовет ошибку AttributeError:

>>> from unittest.mock import Mock
>>> calendar = Mock(spec=['is_weekday', 'get_holidays'])

>>> calendar.is_weekday()
<Mock name='mock.is_weekday()' id='4569015856'>
>>> calendar.create_event()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 582, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'create_event'

Здесь вы указали, что у calendar есть методы .is_weekday() и .get_holidays(). Когда вы обращаетесь к .is_weekday(), он возвращает Mock. При обращении к методу .create_event(), не соответствующему спецификации, Mock выдает ошибку AttributeError.

Уточнения работают так же, если вы конфигурируете Mock с объектом:

>>> import my_calendar
>>> from unittest.mock import Mock

>>> calendar = Mock(spec=my_calendar)
>>> calendar.is_weekday()
<Mock name='mock.is_weekday()' id='4569435216'>
>>> calendar.create_event()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 582, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'create_event'

.is_weekday() доступен для calendar, поскольку вы настроили calendar на соответствие интерфейсу модуля my_calendar.

Кроме того, unittest.mock предоставляет удобные методы автоматического указания интерфейса экземпляра Mock.

Одним из способов реализации автоматических спецификаций является create_autospec:

>>> import my_calendar
>>> from unittest.mock import create_autospec

>>> calendar = create_autospec(my_calendar)
>>> calendar.is_weekday()
<MagicMock name='mock.is_weekday()' id='4579049424'>
>>> calendar.create_event()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 582, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'create_event'

Как и раньше, calendar - это экземпляр Mock, интерфейс которого соответствует my_calendar. Если вы используете patch(), вы можете передать аргумент в параметр autospec, чтобы достичь того же результата:

>>> import my_calendar
>>> from unittest.mock import patch

>>> with patch('__main__.my_calendar', autospec=True) as calendar:
...     calendar.is_weekday()
...     calendar.create_event()
...
<MagicMock name='my_calendar.is_weekday()' id='4579094312'>
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/mock.py", line 582, in __getattr__
    raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'create_event'

Заключение

Вы узнали так много нового об имитации объектов с помощью unittest.mock!

Теперь вы можете:

  • Используйте Mock для имитации объектов в ваших тестах
  • Проверьте данные об использовании, чтобы понять, как вы используете свои объекты
  • Настройте возвращаемые значения и побочные эффекты ваших имитируемых объектов
  • patch() объекты по всей вашей кодовой базе
  • Видеть и избегать проблем при использовании Python mock-объектов

Вы заложили фундамент понимания, который поможет вам создавать более качественные тесты. Вы можете использовать имитаторы для получения информации о вашем коде, которую вы не смогли бы получить иным способом.

Оставляю вам последнее предостережение. Остерегайтесь чрезмерного использования подражаемых объектов!

Легко воспользоваться мощью Python mock-объектов и насмехаться так много, что вы фактически снижаете ценность ваших тестов.

Если вы хотите узнать больше о unittest.mock, я советую вам прочитать его превосходную документацию.

Вернуться на верх