Из этой статьи вы узнаете, что такое мокинг, какими бывают тестовые двойники и в чем различия между ними, а также познакомитесь с некоторыми простыми примерами мокирования с использованием фреймворка Jest.
Что такое мокинг?
Мокинг (mocking) – это способ изолировать блок кода для тестирования, заменяя его зависимости другими объектами, поведение которых можно контролировать.
Зависимостью может выступать все, с чем связана исследуемая часть кода, но обычно это та составляющая системы, которая непосредственно участвует в работе нужного нам блока.
Чтобы изолировать тестируемый блок кода, нам нужно заменить связанные с ним части системы моками (mocks), которые будут имитировать их поведение. Это очень полезно, так как часто бывает так, что реальные зависимости неудобно использовать в тестах.
Если вкратце, то мокинг – это создание части кода, которая будет вести себя, как реальная составляющая вашей системы.
БЕСПЛАТНО СКАЧАТЬ КНИГИ в телеграм канале "Библиотека тестировщика"
Тестовые двойники – пустышка, фейк, заглушка, мок, шпионы
В тестировании существует несколько определений для объектов, которые подражают реальному поведению частей системы. Общий термин – тестовый двойник.
Тестовый двойник – это объект, который может заменить реальную часть кода для тестирования, примерно как дублер заменяет актера в фильме.
Существует пять основных видов тестовых двойников: пустышка (dummy), фейк (fake), заглушка (stub), мок (mock), шпионы (spies).
Пустышки
Бывают ситуации, когда мы используем некоторый объект вместо реального кода, но при этом нам не важно поведение этого объекта. Такой объект и называется пустышкой. Как правило, пустышки используются для заполнения списка параметров, чтобы код компилировался и компилятор “не ругался”.
Фейки
Эти элементы фактически выглядят и работают, как настоящий код нашей системы. Однако представленная в них реализация является своего рода флагом, который используется для модульного тестирования, но совсем не подходит для использования в продакшене. Например, объект базы данных, который мы можем использовать исключительно в тестовых сценариях, в то время как в продакшене используются реальные данные из БД.
Заглушки
Заглушка – это объект, который содержит заранее подготовленные данные и использует их для ответов во время тестирования. Заглушки используются, когда мы не можем или не хотим вовлекать сервисы, которые будут отвечать на наши запросы реальными данными.
Допустим, у нас есть объект, который при вызове метода должен получить данные из базы данных для ответа. Вместо реального модуля мы можем использовать заглушку , которая вернет захардкоженные данные.
Моки
Моки очень похожи на заглушки, но они более ориентированы на “взаимодействие”. Это значит, что мы не только ожидаем, что мок вернет какое-то заранее заданное значение, но и предполагаем, что для получения этих данных будут вызваны конкретные методы в определенном порядке.
Моки используются, когда мы не хотим вызывать код продакшена или когда нет простого способа проверить, что тестируемый код отработал правильно. В качестве примера отлично подойдет функциональность, которая вызывает службу отправки сообщений по электронной почте.
Скорее всего, нам не нужно отправлять письма каждый раз, когда мы запускаем такой тест. Более того, внутри теста бывает нелегко проверить, что было отправлено нужное письмо. Единственное, что мы можем сделать, это посмотреть на выходные данные той функциональности, которую проверяем в нашем тесте. Другими словами, нам нужно убедиться, что сервис отправки сообщений по электронной почте в принципе вызывался.
Шпионы
Шпионы – это те же заглушки, но записывающие информацию о том, кто, как и когда их вызвал. Для примера снова подойдет сервис отправки сообщений по электронной почты, который фиксирует количество отправленных сообщений.
Пример тестового сценария
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-1024x401.png)
thumb-war.js
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-1-1024x238.png)
thumb-war.test.js
В этом примере мы можем протестировать только то, возвращает ли функция thumbWar
один из переданных ей параметров. Как легко можно догадаться, это далеко не исчерпывающий тест. Поэтому мы будем мокать функцию getWinner
.
Самый простой способ мокинга – это monkey patching.
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-2-1024x493.png)
thumb-war.test.js
Monkey patching – это техника, позволяющая добавлять и изменять поведение части кода системы прямо во время его выполнения без изменения исходного кода.
Таким образом, в нашем примере мы мокаем реализацию функции getWinner
после её импорта, а затем восстанавливаем оригинальную реализацию после завершения теста.
Однако так делать не рекомендуется, поскольку мы присваиваем значение методу импортированного модуля, что нарушает правило eslint – import/namespace.
Примечание: Jest, по сути, делает то же самое при мокинге, но выполняет некоторую “магию” на фоне и мокирует целую систему модулей, что позволяет коду оставаться совместимым и избегать предупреждений/ошибок от компилятора .
Мокинг в Jest
Когда речь идет о мокинге в Jest, обычно имеется в виду замена зависимостей на Mock-функции, встроенные в Jest.
Mock-функция позволяет нам тестировать связи между частями кода, осуществляя следующие действия:
- стирание фактической реализации функции
- перехват вызова функции (включая передаваемые в нее параметры)
- настройка возвращаемых значений во время тестирования.
Цель мокирования – заменить то, что мы не контролируем, на то, что мы можем контролировать. Поэтому важно, чтобы то, чем мы заменяем реальные блоки кода, имело всю необходимую функциональность.
Плагины для мокинга в Jest
jest.fn()
Самый простой способ создать Mock-функцию – это jest.fn()
.
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-3-1024x162.png)
Допустим, у нас есть набор некоторых функций для сложения, вычитания, умножения и деления переданных параметров.
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-4-1024x233.png)
Мы импортируем функции из math.js
и будем использовать их в нашем примере.
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-5-1024x559.png)
Теперь, чтобы протестировать функции doAdd
и doSubtract
, нам нужно создать моки для функций сложения и вычитания с помощью jest.fn()
.
Каждый раз, когда мы будем вызывать функции сложения и вычитания в этом тесте, Jest будет перехватывать этот вызов и использовать вместо них мокированные функции сложения и вычитания.
Возможно, вы уже обратили внимание, что эти операции очень похожи на те, которые мы с вами делали ранее в рамках обзора термина monkey patching. Однако в примере выше Jest фактически имитирует систему модулей и выполняет некоторые внутренние настройки, чтобы наш код выглядел соответствующим стандартам, так что мы не получим никаких ошибок ESLint.
В этом примере мы не тестируем реализацию функций, мы лишь проверяем, были ли эти функции вызваны правильно. Такой тест полезен в случаях, когда мы хотим убедиться, вызывается ли обработчик события, переданный в качестве компонента.
Jest Spy
Еще один способ создания моковой функции – метод jest.spyOn()
. Подобно jest.fn()
, этот метод создает управляемый и изменяемый мок.
Основное отличие заключается в том, что по умолчанию такой метод будет вызывать исходную реализацию. Это полезно, когда вам нужно просто отслеживать вызов метода, сохраняя при этом его исходную реализацию. В других же случаях вам может понадобиться замокать исходную реализацию, но восстановить её позже в тестовом прогоне.
Для любого из этих примеров можно использовать jest.spyOn
.
jest.spyOn()
хранит в памяти исходную реализацию, поэтому в случае, если она была переопределена, jest.spyOn()
позволит нам восстановить изначальную версию, используя встроенный метод mockRestore()
.
Пример простого использования jest.spyOn()
:
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-6-1024x428.png)
Давайте создадим моковую функцию, а затем восстановим исходную реализацию с помощью mockRestore
:
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-7-1024x495.png)
Важно помнить о том, что jest.spyOn
– это просто более удобный синтаксис для базового использования jest.fn()
.
Мы можем получить тот же результат, просто сохранив исходную реализацию. Давайте установим нашу моковую реализацию идентично исходной и затем восстановим исходную реализацию.
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-8-1024x800.png)
В начале теста мы сохраняем функцию math.add
в переменную originalAdd
. Затем мы мокаем реализацию math.add
и тестируем уже подмененную реализацию. А в конце мы восстанавливаем math.add
обратно в originalAdd
.
Строго говоря, именно это и делает jest.spyOn()
, но в более удобном виде.
Разница между использованием jest.fn() и jest.spyOn()
Между функциями jest.fn()
и jest.spyOn()
нет существенной разницы, однако используются они для разных случаев.
Вам подойдет jest.fn()
, если:
- Вы хотите замокать некоторую функцию и вам совершенно неважна ее исходная реализация (эта функция будет переопределена с помощью
jest.fn()
) - Вам нужно подменить только возвращаемое значение как-нибудь метода
- Вы хотите убрать зависимости от бэкенда (например, при вызове API) или сторонних библиотек в ваших тестах
- Вы хотите писать настоящие юнит-тесты. В таком случае вам не важно, правильно ли работает определенная функция, которую вызывает тестируемый модуль, потому что это не является частью вашего теста
jest.spyOn()
подойдет для следующих случаев:
- Исходная реализация функции важна для вашего теста, но в хотите добавить свою собственную реализацию только для конкретного сценария, а затем снова сбросить ее с помощью
mockRestore()
- Вы просто хотите узнать, вызывалась ли тестируемая функция
Мокинг модуля
Мы можем подменить модуль целиком с помощью jest и Jest.mock
. Когда мы мокируем модуль, Jest просто перехватывает вызовы всех функций этого модуля и заменяет их мок-функциями.
В этом примере мы сымитируем модуль Math.js
:
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-9-1024x530.png)
Получается, что вызов jest.mock('./math.js')
по сути устанавливает math.js
во все функции ниже:
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-11-1024x161.png)
Это может пригодиться, когда вам нужно протестировать несколько функций одного модуля. Таким образом, вам не нужно будет мокировать каждую функцию отдельно.
Мокинг API
Метод jest.mock()
также удобно использовать для мокирования ответов API:
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-10-1024x240.png)
index.js
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-12-1024x836.png)
index.test.js
В этом примере, когда мы мокируем модуль axios
, все функции, находящиеся внутри axios
, заменяются на jest.fn()
. Затем мы возвращаем имитацию ответа из метода axios.get
, используя mockResolvedValue
.
Однако, есть несколько проблем с тестированием API таким способом:
- Фактически, в нашем примере мы не делаем запрос к API. Мы мокируем обработчик API и подменяем ответ для метода
get
этого обработчика. - Такой тест оставляет место для очень большого количества ошибок, которые могут возникнуть во время реального запроса к API. Поскольку мы просто подменяем ответ в нашем тесте, то мы не можем проверить, передает ли реальный пользователь обязательные заголовки или параметры запроса в API.
- В этом примере мы замокали модуль
axios
и теперь это является зависимостью в нашем тесте. В будущем, если мы решим перейти с модуляaxios
на, например,fetch
, наши тесты начнут падать, потому что в тесте мы замокали детали реализации.
Все эти проблемы можно решить, замокав обработчик API на более низком уровне, но, пожалуй, это будет очень трудоемко.
Здесь к нам на помощь приходит MSW.
MSW
MSW расшифровывается как Mock Service Worker. Это библиотека для мокирования API, которая использует API Service Worker для перехвата реальных запросов.
API перехватывается на сетевом уровне, поэтому наше приложение ничего не знает о мокировании и действительно выполняет вызов API, что даже можно увидеть во вкладке “Network” консоли браузера. Но ответ на запрос просто возвращается нашим сервером MSW.
Основная идея MSW заключается в том, чтобы создать мок-сервер, который будет перехватывать все запросы и обрабатывать их так же, как если бы их принимал настоящий сервер.
![Код](https://qarocks.ru/wp-content/uploads/2024/02/image-13-857x1024.png)
index.test.js
На этом примере мы вызываем функцию setupWorker
из MSW и передаем обработчик запросов воркеру.
Мы используем обработчик запросов get
, поэтому теперь, после запуска воркера, он будет перехватывать все запросы get
, сделанные к /albumTitles
, и обрабатывать их.
Примечание: В функцию setupWorker
уже встроены обработчики для всех функций rest, а также для graphQl.
Для ответа на ранее перехваченный запрос мы должны указать поддельный ответ, используя функцию response resolver
. Она принимает три аргумента:
req
можно использовать, если нужно получить доступ к какому-то определенному свойству из объекта запроса. Например,req.params
илиreq.headers
и т. д.res
– инструмент для создания поддельного ответаctx
– это группа функций, которые помогают установить необходимые код состояния, заголовки, тело и т.д. для поддельного ответа
Примечание: MockRestore
полезен для тестов в одном файле, но его не обязательно использовать с хуком afterAll
, поскольку каждый файл тестов в Jest находится в изолированной среде.
Перевод статьи «Testing Fundamentals — Mocking».
Пингбэк: Основы работы с мокингом в Python