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

Несколько месяцев назад я решил преобразовать свой хобби-проект - Hackathon Planner API с чистого Javascript на TypeScript и написал об этом в блоге. На этот раз я сел за создание Automated Test Suite и конвейера Continuous Delivery (CD) на его основе.

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

Правильная архитектура для подкожного тестирования

Я большой поклонник подкожного тестирования. Этот тип тестирования начинается прямо под слоем пользовательского интерфейса с большим объемом (который предпочтительно охватывает весь путь до хранилища данных) может иметь большую окупаемость инвестиций. На диаграмме отзывов Кента Бека он будет иметь высокий балл справа: быстрая и широкая обратная связь.

Итак, теперь, когда мы хотим нацелить наши автоматизированные тесты ниже уровня механизма доставки (пользовательский интерфейс, сеть и т. Д.), Здесь возникает важное архитектурное решение. Каковы основные границы моего приложения? С чего начинается моя важная бизнес-логика? Кроме того, как сделать границы приложения очень заметными и понятными для всех разработчиков? Если вы последуете этому пути мышления, одним из хороших результатов будет комбинация паттернов командование и посредник.

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

Чтобы использовать этот шаблон в моем REST API, я создал простой модуль в TypeScript, который позволяет мне выполнять команды и, при необходимости, получать от них результаты. Здесь: TypeScriptCommandPattern.

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

А вот как выглядит обработчик команд в стиле «Hello World». Обратите внимание, что в нем есть четкие определения запросов и ответов. В конце реализации мы убеждаемся, что обработчик зарегистрирован и сопоставлен с запросом. В этой структуре обработчики представляют собой одноэлементные объекты, которые позже могут быть разрешены по типу запроса, а затем выполнены.

Использование этого шаблона в реальном API REST Node.js

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

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

Давайте посмотрим на реализацию одного из этих сценариев и выделим некоторые ключевые свойства. Выберем сценарий в GetIdeas.ts:

  • Строка 3–4: Импортирует базовую структуру обработки асинхронных команд, а также контейнер для регистрации в конце файла.
  • Строка 6–9: внешние модули, которые использует этот обработчик.
  • Строка 11–12: модуль, который используется в нескольких сценариях. Будьте осторожны, я говорю использованный, а не повторно использованный. Помните ошибку повторного использования. Этот модуль (IdeaPrePostProcessor) используется для массажа и дезинфекции сущностей Idea перед их отправкой в ​​пользовательский интерфейс и перед записью в базу данных. Этот модуль используется в нескольких сценариях одинаково и для одинаковых нужд. Для каждого сценария нет незначительных вариаций, поэтому это правильная абстракция и именно поэтому использование, а не повторное использование.
  • Строка 14–16: поскольку наш обработчик является асинхронным (поскольку он обращается к базе данных), он реализует AsyncCommandHandler ‹TRequest, TResponse› и реализует метод HandleAsync, который возвращает обещание объекта GetIdeasReponse.
  • Строка 17–38: Полная вертикальная реализация обработчика. По сути, он сканирует базу данных на предмет всех идей, сортирует их на основе алгоритма, инкапсулирует их в объект ответа и возвращает. Обратите внимание, что в строке 25 используется ключевое слово «await». Это простой и легко читаемый способ представления асинхронного кода. Он существует в последних версиях TypeScript, а также в ES7.
  • Строка 42–47: объекты запроса и ответа, используемые этим обработчиком, определяются и экспортируются. Эти объекты также должны быть доступны для остальной части приложения.
  • Строка 50–52. Создается экземпляр одноэлементного обработчика и регистрируется в соответствии с типом объекта запроса. В зависимости от потребностей приложения это можно сделать более сложным и изощренным. Статически типизированные языки включают в себя множество идей, связанных с контейнерами IoC, тогда как языки с динамической типизацией, такие как javascript, не так много. Также имейте в виду, что эти универсальные конструкции TypeScript существуют только во время разработки. Их нет в транспилированном javascript, что делает невозможным обнаружение или сканирование этих конструкций во время выполнения.

Простые и глупые маршруты ExpressJS

Если у вас есть опыт создания веб-приложений типа MVC или REST API, вы, вероятно, знакомы с идеей контроллеров. То же понятие и в ExpressJS с маршрутами. Хорошая дисциплина - содержать ваши маршруты и контроллеры в чистоте и порядке. Помните, что мы хотим ограничить нашу основную логику приложения и стараемся максимально не допускать ее утечки наружу. Ни к фреймворкам, ни к внешним библиотекам.

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

Тестирование всех сценариев

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

  • Удалите слой ExpressJS, который обычно находится сверху. Это механизм доставки, и он мне не обязательно нужен для проверки моей бизнес-логики, поскольку я уже четко отделил эту логику от этого уровня.
  • Загрузите все обработчики сценариев вместе с реальным хранилищем данных (или реалистичным эмулятором).
  • Снимайте запросы и проверяйте ответы.
  • В некоторых случаях, если одной проверки ответа недостаточно, можно пойти дальше и напрямую проверить хранилище данных на предмет наличия побочных эффектов. В моем случае в этом не было необходимости, я мог и действовать, и проверять, используя только своих обработчиков.

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

Вот несколько ключевых свойств этих тестов:

  • Строки 17–26: перед каждым тестом создаются новые таблицы NoSql, а после каждого теста они удаляются. Это делает каждый тестовый запуск в изолированной программной среде изолированным друг от друга. Таким образом, все они могут работать в любом случайном порядке. В общем, это характеристика юнит-теста, а не интеграционного. Итак, у нас есть лучшее из обоих миров!
  • Все тесты выполняются с использованием локального эмулятора DynamoDb. Так что обратная связь, поступающая из базы данных, реальна. Я верю, что команда Amazon, стоящая за DynamoDb, следит за тем, чтобы эмулятор вел себя точно так же, как настоящий эмулятор в облаке.
  • Интеграционные тесты идут медленно, правда? Нет, если вы не тестируете пользовательский интерфейс, не выполняете много сетевых вызовов или не используете медленные устройства хранения данных. Эти тесты не входят в сложную задачу тестирования пользовательского интерфейса; они не запускают веб-сервер и не совершают чрезмерное количество сетевых вызовов; и они используют эмулятор базы данных в памяти, работающий на том же компьютере. Эти 15 тестов выполняются за ~ 2 секунды, даже если каждый из них настраивает и удаляет свои таблицы данных. Таким образом, вы можете запустить довольно много из них за несколько минут.
  • По своей природе интеграционные тесты обширны. Они дают широкую обратную связь. Но это компромисс, потому что, когда они терпят неудачу, вам нужно копнуть немного глубже, чтобы понять, что именно не удалось по сравнению с крошечными и сфокусированными юнит-тестами. Но в целом я предпочитаю идти на такой компромисс.
  • Сказав это, я ни в коем случае не пытаюсь отбросить здесь ценность модульных тестов. Если я увижу необходимость - как любой фрагмент кода, который важен для системы и делает интересные вещи - я пойду и проведу модульное тестирование этой части изолированно. Особенно код, который требует много алгоритмической работы. Таким образом, модульные тесты ценны, когда они реализованы для правильного кода в соответствии с правильной абстракцией.
  • И последнее, но не менее важное: тесты этого типа (то есть подкожные) взаимодействуют с системой прямо за пределами значимой границы. Это означает, что до тех пор, пока требования к функциям не изменятся, эти тесты остаются действительными и полезными. Они могут пережить большие рефакторинги, потому что не связаны с деталями реализации! На мой взгляд, это очень важно.

В следующей и последней части этой серии я хотел бы рассказать о своем опыте настройки конвейера компакт-дисков с помощью CircleCI, который переносит все файлы TypeScript, устанавливает зависимости, запускает тесты и развертывает в AWS Elastic Beanstalk ».

Спасибо за чтение.

~ Хакан