×Закрыть

Contract protection в Angular-приложениях: как организовать runtime checking

Привет, меня зовут Алексей Павленко, я Lead Front-End Engineer в компании iDeals. Мы создаем виртуальные комнаты данных — инструмент для максимально защищенной передачи чувствительной бизнес-информации. Поэтому наш сервис должен безукоризненно работать в 99,9% случаев. Ошибки на проде могут привести к плохому ux, поэтому для нас критично найти баг раньше, чем это сделает клиент.

Основная задача этого материала — показать, как можно отлавливать и реагировать на неожиданное поведение во время runtime. Рассмотрим способы защиты контрактов как предшествующий этап, остановимся и на проверке типов, непосредственно во время выполнения приложения. Проанализируем существующие решения по runtime checking и как мы можем реагировать с помощью Angular.

Статья будет полезной разработчикам любого уровня синьорности. В частности для тех, кто хочет ознакомится с существующими способами защиты контрактов и внедрить дополнительный шаг, ориентированный на повышения качества. Актуально это будет для создателей тех приложений, где на первый план выходит бесперебойная работа 24/7 и высокий уровень user experience. Но и в целом этот подход будет полезен всем, кто хочет добавить дополнительный уровень защиты API, особенно компаниям, использующим third-party API.

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

Какие есть подходы для предотвращения таких проблем? Это такие инструменты:
— Integration testing
— Law-driven contract testing (Prism, Dredd)
— Consumer-driven contract testing (Pact)
— Runtime checking

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

Контрактное тестирование может решить эту проблему. Какие здесь ключевые моменты? Сам контракт, содержащий API, его описание и сценарии взаимодействия. Клиентские тесты, в которых используется «заглушка», автоматически формируемая из контракта, и API-тесты, также генерируемые оттуда. Особенность данного подхода — требования к поведению API идут upstream, от клиента к серверу.

Если говорить про law-driven контрактное тестирование, здесь «законом» выступают API-доки. Когда они корректно написаны, то представляют собой конечную точку, где все написано в правильном и утвержденном виде. Основная задача — убедиться, что все находится на одинаковом уровне/версии.

Runtime Checking

Думаю, что многие сталкивались с тикетами из-за неправильных контрактов. В чем загвоздка? Если разбить runtime checking на этап разработки и этап продакшна, то понятно, что на дев-env этапе мы боремся за качество. Просто сложите время, которое тратит QA на создание и описание тикета и которое тратит разработчик на фикс, — этих потерь можно избежать при runtime checking.

Какой подход мы используем здесь? В случае возникновения exception, мы отправляем его в базу, оттуда — в Kibana. Там настроен триггер, который пересылает уведомление в канал в Slack. Так мы можем зафиксировать ошибку раньше, чем к нам обратиться клиент.

Если вообще говорить о runtime checking, к нам это пришло, когда Elm стал популярным. Elm — функциональный язык, предназначенный для создания графических интерфейсов. Основное его положение гласит: «No runtime exceptions in practice». Я просмотрел библиотеки, в которых есть runtime check (см. ссылки на репозитории в конце статьи).

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

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

Для примера я взял библиотеку Zod. Что мне в ней понравилось, так это type inference. Вот для наглядности PersonalInfoSchema, которую мы можем потом валидировать. Кроме того, потом мы можем наследовать ее как тип, тем самым убирая дупликацию кода (которая бывает в библиотеках без вывода типов).

Кроме того, у этой библиотеки отличный набор функций для работы с объектами. Можно добавлять схему значений в зависимости от поведения, можно их перезаписывать, убирать из схемы — или на ее основе создавать новую. Также она умеет работать с enums, поддерживает кастомную валидацию: прямо на этапе runtime можно проверить, скажем, совпадает или не совпадает возраст.

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

Из минусов этой библиотеки — не хватает read-only свойства, обязательность всех полей.

Если сравнить ее с популярными на GitHub библиотеками, скажем, c yup, то в последней — не до конца верная проверка типов: она валидирует даже пустой объект. С другой стороны, библиотека io-ts в приоритет ставит функциональное программирование, а не удобство разработчика. Здесь сложности возникают, если какое-либо из значений является optional. Для данной реализации нужно создавать две отдельных схемы с optional и не optional значениями — и объединять их в одну.

А вот лидер по числу звездочек на GitHub, happi/joi, вообще не поддерживает static type inference.

У runtypes — ограниченный набор функций по работе с объектами. Зато у нее есть readonly, можно работать как с объектами, так и с массивами.

Как же все-таки реагировать на неожиданное поведение с помощью упомянутых решений? Давайте посмотрим на примере библиотеки ts.data.json. Основной пункт здесь — создание декодера, в который мы можем подставить нужные нам значения (типа IsNull, IsUndefined, и т.д.). Затем используем InMemoryDbService просто чтобы имитировать сервер. После — вызов метода, и самое интересное — это непосредственно обработчик, в котором мы пытаемся декодировать.

Если все ок, создаем новый instance, если нет — отлавливаем ошибку. В ней будет видно, какой декодер «поломался», поле, где произошла ошибка, и значение, которое пришло.

Резюмируя:

— Runtime check — это возможность проверять как внутреннюю разработку, так и внешние контракты.
— При этом усилия на нашей стороне — простой обработчик, которые помогает нам на деве экономить время разработчиков, тестировщиков, а также правильно настроить взаимодействие внутри команды.
— Кроме того, если говорить обо всем API, это нам дает возможность зафиксировать ошибку раньше, чем это сделает клиент, а значит — и отреагировать раньше.

С решениями которые анализировались во время написания данного материала вы можете ознакомится по ссылкам ниже

  1. github.com/pelotom/runtypes
  2. github.com/sindresorhus/ow
  3. github.com/gcanti/io-ts
  4. github.com/kofno/jsonous
  5. github.com/jquense/yup
  6. github.com/joanllenas/ts.data.json
  7. github.com/fabiandev/ts-runtime
  8. github.com/swissmanu/spicery
  9. github.com/vriad/zod
  10. github.com/hapijs/joi


Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Как-то затянул в один проект io-ts и когда дело дошло до кастомных проверок вышло так что декларативного кода стало больше и читаемость усложнилась по сравнению с императивным подходом, в итоге вернул все в зад)
За список инструментов спасибо)

Подписаться на комментарии