<style>.lazy{display:none}</style>Моки, заглушки и контрактное тестирование

Моки, заглушки и контрактное тестирование

Перевод статьи «How to test software, part I: mocking, stubbing, and contract testing».

Содержание

Разработка, управляемая тестами с использованием моков и заглушек

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

БЕСПЛАТНО СКАЧАТЬ КНИГИ в телеграм канале "Библиотека тестировщика"

Юнит-тесты или тесты компонентов (показаны в нижней части нашей пирамиды) недороги в реализации и быстро выполняются. Используйте их в первую очередь. И только когда вам уже будет недостаточно юнит-тестов, переходите к более затратному (как по времени, так и по ресурсам) тестированию – к интеграционным тестам и тестам на основе UI.

пирамида тестирования

Для чего используются моки и заглушки?

Многие думают, что моки и заглушки используются только для юнит и компонентных тестов. Однако я хочу показать вам, как mock-объекты или заглушки можно использовать и на других уровнях тестирования.

Что такое тестирование с использованием моков?

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

Что такое тестирование с использованием заглушек?

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

Смотрите также: “Mock-сервисы для agile-разработки”.

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

Моки и заглушки в юнит-тестах

Я рекомендую использовать моки или заглушки, когда в вашем коде используются внешние зависимости, такие как системные вызовы или доступ к базе данных. Каждый раз, когда вы запускаете тест, вы проверяете работу кода. Поэтому, когда выполняется функция delete или create, вы каждый раз должны создавать или удалять файл. Это неэффективно, и создаваемые или удаляемые файлы на самом деле не нужны для всех тестов. Кроме того, это требует больших затрат на очистку, так как теперь вам придется вручную удалять что-то каждый раз. Это тот случай, когда мок/заглушка может очень сильно помочь.

Допустим, тест создает файл в /tmp/test_file.txt, а затем тестируемая система удаляет его. Проблема здесь в том, что системные вызовы отнимают много времени. В данном случае вы можете сымитировать ответ на вызов файловой системы, что будет намного быстрее, так как результат вернется немедленно.

Также, использование заглушек для имитации внешнего функционала помогает делать тесты независимыми.

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

Примеры тестирования с моками и заглушками

def read_and_trim(file_path)
	return os.open(file_path).rstrip("\n") #метод вызывает системный вызов, чтобы найти файл по указанному пути и прочитать его содержимое, и удалить символ новой строки.

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

  1. Убедиться, что файл, который ищется в тесте по указанному пути, существует; если его не будет, тест завершится неудачей.
  2. Тесту нужно будет дождаться ответа системного вызова; если системный вызов задержится, тест завершится неудачей.

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

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

@unittest.mock.patch("builtins.open", new_callable=mock_open, read_data="fake file content\n")
def test_read_and_trim_content(self, mock_object):

    self.assertEqual(read_and_trim("/fake/file/path"), "fake file content")
    mock_object.assert_called_with("/fake/file/path")

Мы используем Python mock patch для имитации встроенного вызова open. Таким образом, в тесте проверяется только нужный нам код.

Еще один хороший пример использования моков и заглушек в компонентном тестировании – имитация работы с базой данных. Допустим, нужно протестировать, удаляет ли ваша функция объект из базы данных. Для первого теста вы вручную создаете файл, который нужно удалить. Тест пройдет успешно. Но когда его будет запускать кто-то другой, он может не знать, что нужно вручную создавать файл. И тогда тест упадет. Файл не получится удалить, так как запускающий не знал, что нужно сначала создавать объект. Такой тест не является независимым.

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

Моки и заглушки для внутренних функций

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

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

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

Моки в интеграционном тестировании

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

При интеграционном тестировании вы должны проверять только те компоненты и функциональность, которые сможете изменять. Для этого можно использовать моки и заглушки. Сначала определите интеграции, которые важно тестировать. Затем решите, какие внешние или внутренние сервисы можно заменить моками.

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

@unittest.mock.patch('Github')
def test_parsed_content_from_git(self, mocked_git):
   expected_decoded_content = "b'# Sample Hello World\n\n> How to run this app\n\n- installation\n\n dependencies\n"
   mocked_git.get_repo.return_value = expected_decoded_content

   parsed_content = read_parse_from content(repo='my/repo',
                                            file_to_read='README.md')

   self.assertEqual(parsed_content['titles'], ['Sample Hello World'])

В приведенном выше коде теста метод read_parse_from_content интегрирован с классом, который парсит объект JSON из метода API GitHub. В этом тесте мы проверяем интеграцию между двумя классами.

Использование мока в тесте ускорит его выполнение и сделает менее зависимым, поскольку не будет необходимости обращаться к API GitHub. Также мы сэкономим время и уменьшим нагрузку, поскольку окружению, в котором будет выполняться тест, не нужен будет доступ к внешней сети. Однако, для надежного тестирования с использованием моков вместо внешних сервисов очень важно хорошо разбираться, как эти сервисы будут вести себя в реальности. Если значение expected_decoded_content в примере выше не будет соответствует тому, как GitHub возвращает содержимое файла репозитория, неправильно написанный мок может привести к неожиданным ошибкам. Перед написанием теста, лучше всего сохранить реальную запись вызова внешнего сервиса, чтобы использовать этот вызов в качестве ответа от мока. Имитируемый ответ не должен часто меняться, поскольку версии API почти всегда обратно совместимы. Однако важно регулярно проверять API на случай непредвиденных изменений.

тестирование с внешним сервисом
схема с использованием заглушки

Моки и заглушки в контрактном тестировании (в архитектуре микросервисов)

Когда два разных сервиса интегрируются друг с другом, у каждого из них есть стандарты того, что они посылают и что ожидают получить в ответе. Мы можем считать это контрактами между интегрированными эндпоинтами. Благодаря стандартизации, для тестирования интеграции можно использовать контрактное тестирование.

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

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

Тестирование на основе контрактов позволяет проверить два важных аспекта:

  1. Проверка подключения эндпоинта, который был согласован
  2. Проверка ответа от эндпоинта, с заданным аргументом

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

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

Иногда бывает полезно запускать один и тот же тест в разном окружении с разной конфигурацией. Контрактное тестирование – один из ярких примеров такого случая. Мы можем проверять разные кейсы, меняя окружение и конфигурацию. Когда это нижний уровень окружения, такой как Dev, запуск теста с контрактом-заглушкой позволяет изолированно проверить нашу внутреннюю функцию. А когда дело доходит до окружения верхнего уровня, такой как QA или Staging, тот же тест можно использовать уже с реальным подключением внешнего сервиса. Mbtest – один из инструментов, который может помочь с тестированием контрактов и ответами в заглушках.

тестирование сервиса погоды с заглушками

Заключение

Мы рассмотрели примеры различных уровней тестирования с помощью моков и заглушек. Давайте вспомним, почему они полезны:

  1. Тесты с моками и заглушками проходят быстрее, потому что вам не нужно связываться с внешними сервисами. Не нужно ждать ответа от них.
  2. С помощью моков вы можете гибко изменять масштаб тестирования, чтобы охватить только те части, которые вы можете контролировать и изменять. При использовании внешних сервисов вы бессильны в случае, если в них есть ошибка, и сложнее разобраться, почему тест не работает.
  3. Использование моков для запросов к внешним API поможет вашим тестам быть более надежными.
  4. Контрактное тестирование позволяет командам микросервисов быть более самостоятельными при разработке.

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *