Джо Дюран

В Starchup мы используем JSON REST API, встроенный в Loopback 2.x, для предоставления данных клиентам. Каждое мобильное приложение, веб-приложение и микросервис используют этот API, поэтому очень важно, чтобы он работал надежно. Недавно мы приступили к созданию модульных тестов для него, но с одним недостатком — мы хотели, чтобы тесты писались сами!

Верно. Здесь, в Starchup, мы вывели лень на новый уровень: тесты для самостоятельного написания. Мы осмелились представить себе мир, в котором добавление определения модели в наше приложение Loopback будет не только генерировать конечные точки REST для этой модели, но и тесты, подтверждающие, что конечные точки работают!

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

План

Наш TestBuilder будет запрашивать API, выполняя HTTP-запросы, которые наши клиенты делают на регулярной основе. Как вы, возможно, знаете, модели Loopback поставляются с множеством очень полезных конечных точек и помощников для отношений, но мы сузили их до основных операций CRUD для самой модели: GET по идентификатору, PUT по идентификатору, DELETE по идентификатору и POST.

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

  • Генерировать случайные (но уместные) данные для атрибутов модели
  • Создайте экземпляр модели и убедитесь, что возвращенный экземпляр соответствует отправленному.
  • Получите экземпляр, который мы только что создали, по идентификатору и убедитесь, что он соответствует отправленным нами данным.
  • Обновите экземпляр с другими данными и убедитесь, что данные были обновлены правильно.
  • Удалите экземпляр и убедитесь, что он был удален.
  • Очистить и выйти

Установка

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

Мы сделали то же самое для выхода из системы — на этот раз after запускается тестовый блок.

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

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

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

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

Теперь давайте подробнее рассмотрим, как работает TestBuilder.

Детали

Построение модели

Первая проблема, с которой мы столкнулись, заключалась в том, как сгенерировать данные для модели с правильными ключами и типами данных, чтобы создать действительный экземпляр модели, не зная атрибутов модели. Мы должны были бы получить список свойств программно и иметь возможность определить, какие из них необходимы для построения. Это означало знание имен всех свойств модели, а также типов данных, которые они принимали. Идентификация типов данных в динамически типизированном Javascript — непростая задача, но Loopback предоставил некоторые полезные функции самоанализа, которые сделали эту задачу возможной.

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

В generatePropertyValue() мы получаем тип данных для каждого из свойств модели.

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

Однако это сработало не для всех моделей. Многие из тестов создания все еще терпели неудачу.

Прохождение проверок

Иногда правильных свойств и типов данных было недостаточно для создания корректных экземпляров модели. В примере с атрибутами телефона, SMS и электронной почты нам нужно было передать строки, которые были допустимыми номерами телефонов и адресами электронной почты, чтобы API не отклонил их при проверке. Мы достигли этого, проверив каждое имя свойства с помощью регулярных выражений. Важно отметить, что это было возможно только потому, что мы относительно одинаково называли атрибуты — все атрибуты электронной почты заканчивались на «email», все атрибуты sms заканчивались на «sms» и т. д.

Создание связанных моделей

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

Поэтому мы написали функцию, которая отображала отношения модели и включала только имена тех, чей тип — «belongsTo». Затем мы намеревались рекурсивно создать все отношения принадлежности, необходимые для создания нового экземпляра модели. Мы снова воспользовались интроспективными возможностями Loopback и тем фактом, что вся система была написана на Javascript.

Однако мы быстро обнаружили, что некоторые из наших моделей принадлежат друг другу по кругу. Модель A принадлежала модели B, которая принадлежала модели C, которая также принадлежала бы модели A. Последовало множество принудительных выходов. Без полной перерисовки нашей схемы базы данных нам нужен был способ ограничить, какие отношения «принадлежность» были фактически созданы, исходя из того, что действительно было необходимо.

Благодаря гибкости Loopback мы смогли установить нужные нам внешние идентификаторы как «обязательные». Если бы отношение принадлежности было «требуемым», мы бы создали его, иначе пропустили бы. Реализация этого дополнительного свойства заняла очень мало времени и решила нашу проблему с рекурсией.

Исключения являются исключительными

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

Сравнение объектов

Сравнение объектов в Javascript, как известно, сложно. Чтобы убедиться, что наш API возвращает тот же объект, который мы отправили, мы перебрали каждое из свойств в возвращенном экземпляре и убедились, что они такие же, как те, которые мы отправили. Для простоты мы решили использовать метод isEqual lodash, чтобы абстрагироваться от типов данных.

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

Вынос

Хотя наш TestBuilder не был полным решением для тестирования, это был отличный способ повысить надежность нашей серверной системы.

  • Сценарий сгенерировал более 200 тестов и был создан за гораздо меньше времени, чем потребовалось бы, чтобы создать их наизусть.
  • С небольшой доработкой этот скрипт мог бы стать основой библиотеки Loopback Factory, мало чем отличающейся от популярной жемчужины Ruby, FactoryGirl.
  • Объекты определения модели Loopback — это просто JSON, и вы можете настроить их для своих собственных коварных целей.
  • Mocha запускает тестовые блоки в нелогичном порядке. Попробуйте поместить несколько журналов консоли в блоки it и describe и посмотрите, что произойдет.

Здесь может иметь место чрезмерная оптимизация — можете ли вы действительно доверять тесту, написанному самим кодом? Что мы на самом деле тестируем здесь, кроме нашего собственного интернет-соединения? В любом случае, я горжусь нашим маленьким TestBuilder, хотя бы как успешным экспериментом.