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

Давайте посмотрим, как написать хорошие автоматические тесты для разработки компонентов на Go и как это сделать с помощью библиотеки RSpec в Ruby on Rails.

Добавление Go в стек технологий нашего проекта

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

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

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

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

Мы решили написать отдельный сервис на Go, который разделял доступ к базе данных с приложением Rails, которое оставалось ответственным за изменения в структуре таблицы. Только с двумя приложениями такая схема с общей базой данных работает нормально. Вот как это выглядело:

Мы написали и развернули сервис в отдельном экземпляре Rails. Таким образом, не нужно было беспокоиться о том, что обработчик запросов будет затронут всякий раз, когда приложение Rails будет развернуто. Сервис принимает HTTP-запросы напрямую без Ngnix и не использует много памяти. Вы можете назвать это минималистичным приложением!

Проблема с модульным тестированием в Go

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

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

Наше решение

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

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

В результате получилась установка:

  1. RSpec компилирует и запускает двоичный файл Go с конфигурацией, в которой указывается доступ к тестовой базе данных вместе с конкретным портом для приема HTTP-запросов, то есть 8082.
  2. Он также запускает утилиту, которая записывает HTTP-запросы, поступающие на порт 8083.
  3. Пишем регулярные тесты в RSpec. Это создает необходимые данные в базе данных и отправляет запрос на localhost: 8082, как если бы это была внешняя служба, такая как HTTParty.
  4. Разбираем ответ, проверяем изменения в базе, получаем список запросов, которые были записаны заменителем RequestBin, и проверяем их.

Детали реализации

Вот как мы это реализовали. В качестве демонстрации вызовем тестовую службу TheService и создадим оболочку:

Стоит отметить, что при использовании RSpec файлы автозагрузки должны быть настроены в папке поддержки:

Dir[Rails.root.join('spec/support/**/*.rb')].each {|f| require f}

Метод запуска:

  • Читает информацию о конфигурации, необходимую для запуска TheService. Эта информация может отличаться у разных разработчиков и поэтому исключена из Git. Конфигурация содержит необходимые настройки для запуска программы. Все эти различные конфигурации собраны в одном месте, поэтому вам не нужно создавать ненужные файлы.
  • Компилируется и запускается go run <path to main.go> <path to config>
  • Опрашивает каждую секунду и ждет, пока TheService будет готов принимать запросы.
  • Записывает идентификатор каждого процесса, чтобы ничего не повторять и иметь возможность остановить процесс.

Сама конфигурация:

Метод «стоп» просто останавливает процесс. Но есть одна хитрость! Ruby запускает команду «go run», которая компилирует TheService и запускает двоичный файл в дочернем процессе с неизвестным идентификатором. Если мы просто остановим процесс, выполняющийся в Ruby, дочерний процесс не остановится автоматически, и порт останется в использовании. Таким образом, остановка TheService должна пройти через идентификатор группы процессов:

Затем мы подготавливаем «shared_context», в котором мы определяем переменные по умолчанию, запускаем службу TheService, если она еще не запущена, и временно выключаем видеомагнитофон, поскольку видеомагнитофон будет видеть то, что мы делаем, как запрос внешней службы, но мы не хотим Видеомагнитофон для имитации запросов на этом этапе:

А теперь посмотрим на написание самих спецификаций:

Служба может отправлять HTTP-запросы к внешним службам. Мы можем настроить его для перенаправления запросов в локальную утилиту, которая их регистрирует. Для этой утилиты также есть оболочка для ее запуска и остановки, аналогичная «TheServiceControl», за исключением того, что эту утилиту можно просто запустить как двоичный файл без компиляции.

Дополнительные особенности

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

Если вы специально запустите спецификации, которые не нуждаются в TheService, он не запустится.

Чтобы не тратить время на запуск TheService каждый раз при изменении спецификации, в процессе разработки вы можете запускать TheService вручную в терминале и просто не отключать его. Когда это необходимо, вы даже можете запустить его в режиме отладки IDE. Затем спецификации готовят все, отправляют запрос в сервис, он останавливается, и вы можете легко его отлаживать. Это делает подход TDD действительно удобным.

Заключение

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