Обсуждение базовой настройки Mocha для запуска интеграционных тестов

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

Но что, если вы хотите повысить безопасность, а также написать интеграционные тесты в приложении Node.js? В этой статье я расскажу о базовой настройке Mocha для запуска интеграционных тестов.

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

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

Если что-то еще разбилось, мы не особо беспокоились об этом, но это было в значительной степени концом света.

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

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

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

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

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

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

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

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

Итак, на этом этапе мы упустили все шансы создать лучший набор тестов. Интерфейс был довольно сложным и написан без учёта тестов.

Но не волнуйтесь. Эта статья не о том, что мы потеряли надежду на полноценный тестовый объем.

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

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

Мой разум был потрясен. Такое простое и элегантное решение. Вы можете запускать тесты для любой базы данных, которая находится в ее последнем состоянии с самыми последними данными. Мне было любопытно, насколько сложно будет реализовать такое решение в Node.js.

Итак, я создал пример проекта с Экспрессом.

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

Данные хранятся в базе данных MySQL, доступ к которой осуществляется с помощью пакета MySQL npm. Если вы используете другую базу данных или пакет, вам, вероятно, потребуется настроить это решение.

Хватит тарабарщины, давайте код!

Кодирование

Во-первых, нам понадобится класс util для связи с базой данных.

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

В connect мы подключаемся к базе данных и перебираем все методы прототипа. Мы оборачиваем их util.promisify, чтобы мы могли использовать async await (пакет MySQL поддерживает обратный вызов, и я не хотел проходить через этот ад).

Метод запроса подключается к db, если он еще не подключен, и выполняет запрос на нем.

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

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

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

У нас есть все - можем протестировать!

В качестве тестового бегуна я выбрал Mocha, но он будет похож на других бегунов. Это мои тестовые примеры:

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

Но этот тестовый пример проверяет именно то, что мы хотим - начальное состояние db должно быть одинаковым для каждого теста.

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

Если вы сравните это с утверждениями, вы увидите, что мы ожидаем разные значения.

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

Сразу после создания соединения вам нужно вызвать beginTransaction в соединении, и все готово.

Итак, нам нужно сделать именно это:

Если вы не видели method.call(this, …), вы можете прочитать об этом на сайте w3schools. Вкратце, он устанавливает контекст для данного вызова функции.

Итак, мы вызываем родительский метод (исходная реализация) и говорим, что для него установлен экземпляр mockedDb.

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

Вот и все! Шучу ... Если вы внимательно прочитали, то можете вспомнить, что у нас есть createdAll, который использует транзакции и звонки commit.

Это вызовет “should return 4 books in create many tries to create 2 books”, это действительно зафиксирует транзакцию и испортит наши данные в базе данных.

Чтобы эта боль исчезла, нам нужно заглушить commit метод, rollback метод и beginTransaction в соединении.

Это причина того, что в базе данных ничего не произойдет, когда мы вызовем эти методы в реализации.

Наш MockedDb класс будет выглядеть так:

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

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

Вы можете найти весь проект на моем GitHub.

Надеюсь, вам понравилась эта статья.

Ваше здоровье!