Русский
Русский
English
Статистика
Реклама

Manychat

На пути к бессерверным базам данных как и зачем

13.08.2020 12:11:08 | Автор: admin
Всем привет! Меня зовут Голов Николай. Раньше я работал в Авито и шесть лет руководил Data Platform, то есть занимался всеми базами: аналитическими (Vertica, ClickHouse), потоковыми и OLTP (Redis, Tarantool, VoltDB, MongoDB, PostgreSQL). За это время я разобрался с большим количеством баз данных самых разных и необычных, и с нестандартными кейсами их использования.

Сейчас я работаю в ManyChat. По сути это стартап новый, амбициозный и быстро растущий. И когда я только вышел в компанию, возник классический вопрос: А что сейчас стоит брать молодому стартапу с рынка СУБД и баз данных?.

В этой статье, основанной на моем докладе на онлайн-фестивале РИТ++2020, отвечу на этот вопрос. Видеоверсия доклада доступна на YouTube.



Общеизвестные базы данных 2020 года


На дворе 2020 год, я огляделся и увидел три типа БД.

Первый тип классические OLTP базы: PostgreSQL, SQL Server, Oracle, MySQL. Они написаны давным-давно, но по-прежнему актуальны, потому что хорошо знакомы сообществу разработчиков.

Второй тип базы из нулевых. Они пытались уйти от классических шаблонов путем отказа от SQL, традиционных структур и ACID, за счёт добавления встроенного шардирования и других привлекательных фич. Например, это Cassandra, MongoDB, Redis или Tarantool. Все эти решения хотели предложить рынку что-то принципиально новое и заняли свою нишу, потому что в определенных задачах оказались крайне удобными. Эти базы обозначу зонтичным термином NOSQL.

Нулевые закончились, к NOSQL базам привыкли, и мир, с моей точки зрения, сделал следующий шаг к managed базам. У этих баз ядро то же, что и у классических OLTP баз или новых NoSQL. Но у них нет потребности в DBA и DevOps и они крутятся на управляемом железе в облаках. Для разработчика это просто база, которая где-то работает, а как она установлена на сервере, кто настроил сервер и кто его обновляет, никого не волнует.

Примеры таких баз:
  • AWS RDS managed обертка над PostgreSQL/MySQL.
  • DynamoDB AWS аналог document based базы, похоже на Redis и MongoDB.
  • Amazon Redshift managed аналитическая база.

В основе это старые базы, но поднятые в managed среде, без необходимости работы с железом.

Примечание. Примеры взяты для среды AWS, но их аналоги существуют также в Microsoft Azure, Google Cloud, или Яндекс.Облаке.



Что же из этого нового? В 2020 году ничего из этого.

Концепция Serverless


Действительно новое на рынке в 2020 году это serverless или бессерверные решения.

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

Можно ли как-то иначе? C бессерверными сервисами можно.

В чем фокус этого подхода: нет сервера, нет даже аренды виртуального instance в облаке. Для развертывания сервиса копируем код (функции) в репозиторий и опубликовываем наружу endpoint. Дальше просто платим за каждый вызов этой функции, полностью игнорируя аппаратное обеспечение, где она выполняется.

Попытаюсь проиллюстрировать этот подход на картинках.


Классический деплой. У нас есть сервис с определенной нагрузкой. Поднимаем два инстанса: физические серверы или инстансы в AWS. На эти инстансы направляются внешние запросы, которые там обрабатываются.

Как видно на картинке, серверы утилизированы неодинаково. Один утилизирован на 100%, там два запроса, а один только на 50% частично простаивает. Если придет не три запроса, а 30, то вся система не справится с нагрузкой и начнет тормозить.



Бессерверный деплой. В бессерверном окружении у подобного сервиса нет инстансов и серверов. Есть некоторый пул разогретых ресурсов маленьких подготовленных Docker-контейнеров с развернутым кодом функции. Система получает внешние запросы и на каждый из них бессерверный фреймворк поднимает маленький контейнер с кодом: обрабатывает именно этот запрос и убивает контейнер.

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

Концепция публикации бессерверной функции подходит для stateless сервиса. А если вам нужен (state) statefull сервис, то к сервису добавляем базу данных. В этом случае, когда доходит до работы со state, с состоянием, каждая statefull функция просто пишет и читает из базы данных. Причем из базы данных любого из трех типов, описанных в начале статьи.

Какое общее ограничение у всех этих баз? Это расходы на постоянно используемый облачный или железный сервер (или несколько серверов). Неважно, используем классическую базу или managed, есть Devops и админ или нет, всё равно 24 на 7 платим за железо, электричество и аренду дата-центра. Если у нас классическая база, мы платим за master и slave. Если высоконагруженная шардированная база платим за 10, 20 или 30 серверов, и платим постоянно.

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

Serverless база данных теория


Вопрос 2020 года: можно ли базу данных тоже сделать serverless? Все слышали про serverless бэкенд а давайте и базу данных попробуем сделать serverless?

Это звучит странно, потому что база данных это же statefull сервис, не очень подходящий для serverless инфраструктуры. При этом, и state у базы данных очень большой: гигабайты, Терабайты, а в аналитических базах даже петабайты. Его так просто в легковесных Docker-контейнерах не поднять.

С другой стороны, практически все современные базы это огромное количество логики и компонентов: транзакции, согласование целостности, процедуры, реляционные зависимости и много логики. Довольно большой части логики базы данных достаточно небольшого state. Гигабайты и Терабайты непосредственно используются только небольшой частью логики базы данных, связанной с непосредственным выполнением запросов.

Соответственно, идея: если часть логики допускает stateless исполнение, почему бы не распилить базу на Stateful и Stateless части.

Serverless для OLAP решений


Давайте посмотрим, как может выглядеть распилка базы данных на Stateful и Stateless части на практических примерах.



Например, у нас есть аналитическая база данных: внешние данные (красный цилиндр слева), ETL-процесс, который загружает данные в базу, и аналитик, который отправляет к базе SQL-запросы. Это классическая схема работы хранилища данных.

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

Рассмотрим альтернативный подход, реализованный в базе AWS Athena Serverless. Здесь нет постоянно выделенного железа, на котором хранятся загруженные данные. Вместо этого:

  • Пользователь отправляет SQL-запрос к Athena. Оптимизатор Athena анализирует SQL-запрос и ищет в хранилище метаданных (Metadata) конкретные данные, нужные для выполнения запроса.
  • Оптимизатор, на основе собранных данных, выгружает нужные данные из внешних источников во временное хранилище (временную базу данных).
  • Во временном хранилище выполняется SQL-запрос от пользователя, результат возвращается пользователю.
  • Временное хранилище очищается, ресурсы высвобождаются.


В этой архитектуре мы платим только за процесс выполнение запроса. Нет запросов нет расходов.



Это рабочий подход и он реализуется не только в Athena Serverless, но и в Redshift Spectrum (в AWS).

На примере Athena видно, что Serverless база данных работает на реальных запросах с десятками и сотнями Терабайт данных. Для сотен Терабайт понадобятся сотни серверов, но нам не нужно за них платить мы платим за запросы. Скорость каждого запроса (очень) низка по сравнению со специализированными аналитическими базами типа Vertica, но зато мы не оплачиваем периоды простоя.

Такая база данных применима для редких аналитических ad-hoc запросов. Например, когда мы спонтанно решим проверить гипотезу на каком-то гигантском объеме данных. Для этих кейсов Athena подходит идеально. Для регулярных запросов такая система выходит дорого. В этом случае кешируйте данные в каком-то специализированном решении.

Serverless для OLTP решений


В предыдущем примере рассматривались OLAP-задачи (аналитические). Теперь рассмотрим OLTP-задачи.

Представим масштабируемый PostgreSQL или MySQL. Давайте поднимем обычный managed instance PostgreSQL или MySQL на минимальных ресурсах. Когда на инстанс будет приходить больше нагрузки, мы будем подключать дополнительные реплики, на которые распределим часть читающей нагрузки. Если запросов и нагрузки нет отключаем реплики. Первый инстанс это мастер, а остальные реплики.

Эта идея реализована в базе под названием Aurora Serverless AWS. Принцип простой: запросы от внешних приложений принимает proxy fleet. Видя рост нагрузки, он выделяет вычислительные ресурсы из предварительно прогретых минимальных инстансов подключение выполняется максимально быстро. Отключение инстансов происходит так же.

В рамках Aurora есть понятие Aurora Capacity Unit, ACU. Это (условно) инстанс (сервер). Каждый конкретный ACU может быть master или slave. Каждый Capacity Unit обладает своей оперативной памятью, процессором и минимальным диском. Соответственно, один master, остальные read only реплики.

Количество этих работающих Aurora Capacity Units это настраиваемый параметр. Минимальное количество может быть один или ноль (в таком случае база не работает, если нет запросов).



Когда база получает запросы, proxy fleet поднимает Aurora CapacityUnits, увеличивая производительные ресурсы системы. Возможность увеличивать и уменьшать ресурсы позволяет системе жонглировать ресурсами: автоматически выводить отдельные ACU (подменяя их новыми) и накатывать на выведенные ресурсы все актуальные обновления.

База Aurora Serverless может масштабировать читающую нагрузку. Но в документации об этом не сказано прямо. Может возникнуть ощущение, что они могут поднимать multi-master. Волшебства же никакого нет.

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

Это решение не подходит для стабильного высокого highload, потому что оно не умеет масштабировать пишущую нагрузку. Все эти подключения и отключения ресурсов происходят в момент так называемого scale point момента времени, когда базу не держит транзакция, не держат временные таблицы. Например, в течении недели scale point может и не случиться, и база работает на одних ресурсах и просто не может ни расшириться, ни сжаться.

Волшебства нет это обычный PostgreSQL. Но процесс добавления машин и отключения частично автоматизирован.

Serverless by design


Aurora Serverless это старая база, переписанная под облака, чтобы использовать отдельные преимущества Serverless. А теперь расскажу о базе, которая изначально написана под облака, под serverless подход Serverless-by-design. Её сразу разрабатывали без предположения о том, что она работает на физических серверах.

Эта база называется Snowflake. В ней три ключевых блока.



Первый это блок метаданных. Это быстрый in-memory сервис, который решает вопросы с безопасностью, метаданными, транзакциями, оптимизацией запроса (на иллюстрации слева).

Второй блок это множество виртуальных вычислительных кластеров для расчетов (на иллюстрации набор синих кружков).

Третий блок система хранения данных на базе S3. S3 это безразмерное объектное хранилище в AWS, нечто вроде безразмерного Dropbox для бизнеса.

Давайте посмотрим, как Snowflake работает, в предположении о холодном старте. То есть база есть, данные в нее загружены, работающих запросов нет. Соответственно, если к базе нет запросов, то у нас поднят быстрый in-memory Metadata сервис (первый блок). И у нас есть хранилище S3, где лежат данные таблиц, разбитые на так называемые микропартиции. Для простоты: если в таблице лежат сделки, то микропартиции это дни сделок. Каждый день это отдельная микропартиция, отдельный файлик. И когда база работает в таком режиме, вы платите только за место, занимаемое данными. Причем тариф за место очень низкий (особенно с учетом значительного сжатия). Сервис метаданных тоже работает постоянно, но для оптимизации запросов много ресурсов не нужно, и сервис можно считать условно-бесплатным.

Теперь представим, что к нашей базе пришел пользователь и кинул SQL-запрос. SQL-запрос сразу поступает на обработку в Metadata сервис. Соответственно, получив запрос, этот сервис анализирует запрос, доступные данные, полномочия пользователя и, если все хорошо, составляет план обработки запроса.

Далее сервис инициирует запуск вычислительного кластера. Вычислительный кластер это кластер из серверов, которые выполняют вычисления. То есть это кластер, который может содержать 1 сервер, 2 севера, 4, 8, 16, 32 сколько захотите. Вы кидаете запрос и под него мгновенно начинается запуск этого кластера. Это реально занимает секунды.



Далее, после того, как кластер стартовал, в кластер из S3 начинают копироваться микропартиции, нужные для обработки именно вашего запроса. То есть, представим, что для выполнения SQL-запроса нужно две партиции из одной таблицы и одна из второй. В таком случае в кластер будут скопированы только три нужные партиции, а не все таблицы целиком. Именно поэтому и именно из-за того, что всё находится в рамках одного дата-центра и соединено очень быстрыми каналами, весь процесс перекачки происходит очень быстро: за секунды, очень редко за минуты, если речь не идет про какие-то чудовищные запросы. Соответственно, микропартиции копируются на вычислительный кластер, и, по завершении, на этом вычислительном кластере выполняется SQL запрос. Результатом этого запроса может быть одна строчка, несколько строчек или таблица они отправляются наружу пользователю, чтобы он выгрузил, отобразил у себя в BI инструменте, или еще как-нибудь использовал.

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

Описанный выше сценарий, от прихода пользователя до поднятия кластера, загрузка данных, выполнение запросов, получение результатов, оплачивается по тарифу за минуты использования поднятого виртуального вычислительного кластера, виртуального warehouse. Тариф варьируется в зависимости от зоны AWS и размера кластера, но, в среднем, это несколько долларов в час. Кластер из четырех машин в два раза дороже, чем из двух машин, из восьми машин еще в два раза дороже. Доступны варианты из 16, 32 машин, в зависимости от сложности запросов. Но вы платите только за те минуты, когда кластер реально работает, потому что, когда запросов нет, вы как бы убираете руки, и после 5-10 минут ожидания (настраиваемый параметр) он сам погаснет, освободит ресурсы и станет бесплатным.

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

Первый сценарий описывал использование Snowflake в однопользовательском варианте. Теперь давайте представим, что пользователей много, что уже ближе к реальному сценарию.

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

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

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

Для большого количества легких запросов можно поднять 2-3 небольших кластера, размером, условно, 2 машины каждый. Это поведение реализуемо, в том числе, с помощью автоматических настроек. То есть вы говорите: Snowflake, подними маленький кластер. Если нагрузка на него вырастет больше определенного параметра, подними аналогичный второй, третий. Когда нагрузка начнет спадать гаси лишние. Чтобы вне зависимости от того, сколько аналитиков приходит и начинает смотреть отчеты, всем хватало ресурсов.

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

При этом, для тяжелых запросов (от Data Scientists), вы можете поднять один очень большой кластер на условные 32 машины. Этот кластер тоже будет оплачиваться только за те минуты и часы, когда там работает ваш гигантский запрос.

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

Подведем итог по Snowflake. База сочетает красивую идею и работоспособную реализацию. В ManyChat мы используем Snowflake для аналитики всех имеющихся данных. Кластеров у нас не три, как в примере, а от 5 до 9, разных размеров. У нас есть условные 16-машинные, 2-машинные, есть и супер-маленькие 1-машинные для некоторых задач. Они успешно распределяют нагрузку и позволяют нам здорово экономить.

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

Соответственно, база хорошо подходит для OLAP-задач. При этом, к сожалению, для OLTP-нагрузок она еще не применима. Во-первых, эта база колоночная, со всеми вытекающими последствиями. Во-вторых, сам подход, когда на каждый запрос по необходимости вы поднимаете вычислительный кластер и проливаете его данными, к сожалению, для OLTP-нагрузок еще недостаточно быстрый. Секунды ожидания для OLAP-задач это нормально, а для OLTP-задач неприемлемо, лучше бы 100 мс, а ещё лучше 10 мс.

Итог


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

Выполнение SQL-запросов тоже можно воспринять как сервисы с легким state, которые могут всплыть в бессерверном режиме, как вычислительные кластера Snowflake, скачать только нужные данные, выполнить запрос и погаснуть.

Serverless базы продакшен уровня уже доступны для использования, они работают. Эти serverless базы уже готовы справляться с OLAP-задачами. К сожалению, для OLTP-задач они применяются с нюансами, так как есть ограничения. С одной стороны, это минус. Но, с другой стороны, это возможность. Возможно, кто-то из читателей найдет способ, как OLTP-базу сделать полностью serverless, без ограничений Aurora.

Надеюсь, вам было интересно. За Serverless будущее :)
Подробнее..

Snowflake, Anchor Model, ELT и как с этим жить

30.11.2020 14:07:23 | Автор: admin
Привет! Меня зовут Антон Поляков, и я разрабатываю аналитическое хранилище данных и ELT-процессы в ManyChat. В настоящий момент в мире больших данных существуют несколько основных игроков, на которых обращают внимание при выборе инструментария и подходов к работе аналитических систем. Сегодня я расскажу вам, как мы решили отклониться от скучных классических OLAP-решений в виде Vertica или Exasol и попробовать редкую, но очень привлекательную облачную DWaaS (Data Warehouse as a Service) Snowflake в качестве основы для нашего хранилища.

С самого начала перед нами встал вопрос о выборе инструментов для работы с БД и построении ELT-процессов. Мы не хотели использовать громоздкие и привычные всем готовые решения вроде Airflow или NiFi и пошли по пути тонкой кастомизации. Это был затяжной прыжок в неизвестность, который пока продолжается и вполне успешно.

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

Описание данных ManyChat




ManyChat это платформа для общения компаний с клиентами через мессенджеры. Нашим продуктом пользуется более 1.8 млн бизнесов по всему миру, которые общаются c 1.5 млрд подписчиков.

Моя команда занимается разработкой хранилища и ELT-платформы для сбора и обработки всех доступных данных для последующей аналитики и принятия решений.

Большую часть данных мы получаем из собственного приложения: нажатия пользователями кнопок, попапов, события и изменения моделей бэкэнда (пользователя/подписчика/темплейтов/взаимодействия с нашим апи и десятки других). Также получаем информацию из логов и исторических данных из Postgres-баз.

Некоторые данные мы принимаем от внешних сервисов, взаимодействие с которыми происходит посредством вебхуков. Пока это Intercom и Wistia, но список постепенно пополняется.

Данные для аналитиков


Аналитики ManyChat для своей работы пользуются данными из слоя DDS (Data Distribution Storage / Service), где они хранятся в шестой нормальной форме (6 нф). По сути, аналитики хорошо осведомлены о структуре данных в Snowflake и сами выбирают способы объединения и обработки множеств с помощью SQL.

В своей ежедневной работе аналитики пишут запросы к десяткам таблиц разного размера, на обработку которых у СУБД уходит определенное время. За счет своей архитектуры Snowflake хорошо подходит для аналитики больших данных и работы со сложными SQL запросами. Приведу конкретные цифры:

  • Размер больших таблиц от 6 до 21 миллиарда строк;
  • Среднее количество просканированных в одном аналитическом запросе микро-партиций 1052;
  • Отношение количества запросов с использованием SSD к запросам без использования локального диска 48/52.

В таблице ниже приведена производительность реальных запросов за последний месяц в зависимости от количества используемых в них объектов. Все эти запросы были выполнены на кластере размера S (запросы от ELT-процессов в данных расчетах не участвовали).

Все запросы
Объектов в запросе Количество запросов AVG Время выполнения (сек) MED Время выполнения (сек)
1 3 15149 33 1.27
4 10 3123 48 8
11 + 729 188 38


Запросы, выполняемые быстрее, чем за 1 секунду, вынесены в отдельную группу. Это позволяет разделить запросы, использующие SSD (локальный кэш и сохраненные данные), от тех, которым приходится основную часть данных читать с медленных HDD.

Запросы > 1 сек
Объектов в запросе Количество запросов AVG Время выполнения (сек) MED Время выполнения (сек)
1 3 5747 71 9
4 10 2301 61 15
11 + 659 201 52


Увеличение количества объектов в запросе усложняет его процессинг.

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

Anchor Model


При раскладке данных в хранилище мы используем классическую якорную модель (Anchor Model). Эта модель позволяет гибко реагировать на изменение уже хранимых или добавление новых данных. Также благодаря ей можно эффективнее сжимать данные и быстрее работать с ними.

Для примера, чтобы добавить новый атрибут к имеющейся сущности, достаточно создать еще одну таблицу и сообщить аналитикам о необходимости делать join'ы на нее.

Подробнее про Anchor Model, сущности, атрибуты и отношения вы можете почитать у Николая Голова aka @azazoth (здесь и здесь).

Немного о Snowflake



Размеры кластеров на примере цветных квадратов с текстом

СУБД выделяет расчетные мощности on-demand, как и во многих других продуктах AWS. Бюджет расходуется только если вы используете предоставленные для расчетов мощности тарифицируется каждая секунда работы кластера. То есть, при отсутствии запросов, вы тратите деньги только на хранение данных.

Для простых запросов можно использовать самый дешёвый кластер (warehouse). Для ELT-процессов, в зависимости от объема обрабатываемых данных, поднимаем подходящий по размеру кластер: XS / S / M / L / XL / 2XL / 3XL / 4XL прямо как размеры одежды. После загрузки и / или обработки выключаем его, дабы не тратить деньги. Время выключения кластера можно настраивать: от тушим сразу, как закончили расчет запроса до никогда не выключать.


Выделяемое на каждый размер кластера железо и цена за секунду работы

Подробнее про кластеры Snowflake читайте тут. А так же в последней статье Николая Голова.

В настоящий момент ManyСhat использует 9 различных кластеров:

  • 2 X-Small для ELT процессов с маленькими наборами данных до миллиарда записей.
  • 4 Small для запросов из Tableau и ELT процессов, требующих больших join'ов и тяжелых расчетов, например, заполнение строкового атрибута. Также этот кластер используется для работы аналитиков по умолчанию.
  • 1 Medium для материализации данных (View Materialization).
  • 1 Large для работы с данными больших объемов.
  • 1 X-Large для единоразовой загрузки / правки огромных исторических данных.

Объем наших данных в Snowflake составляет приблизительно 11 Тбайт. Объем данных без сжатия около 55 Тбайт (фактор сжатия х5).

Особенности Snowflake


Архитектура


Все кластеры в системе работают изолированно. Архитектура решения Snowflake представляется тремя слоями:

  1. Слой хранилища данных
  2. Слой обработки запросов
  3. Сервисный слой аутентификации, метаданных и др.


Иллюстрация архитектуры Snowflake

Snowflake работает с горячими и холодными данными. Холодными считаются данные, лежащие в S3 на обычных HDD (Remote Disk). При запросе они дольше считываются и загружаются в быстрые SSD отдельно для каждого кластера. Пока кластер работает, данные доступны на локальном SSD (Local Disk), что ускоряет запросы в несколько раз по сравнению с работой на холодную.



Помимо этого, существует общий для всех кластеров кэш результата запроса (Result Cache) за последние 24 часа. Если данные за это время не изменились, при повторном запуске одного и того же запроса на любом из кластеров они не будут считаны повторно. Подробнее можно почитать тут.

Микро-партиции


Одной из интересных фичей Snowflake является работа с динамическими микро-партициями. Обычно в базах данных используются статические, но в ряде случаев, например, при перекосе данных (data skew), данные между партициями распределяются неравномерно что усложняет / замедляет обработку запросов.

В Snowflake все таблицы хранятся в очень маленьких партициях, содержащих от 50 до 500 Мбайт данных без сжатия. СУБД хранит в метаданных информацию обо всех строках в каждой микро-партиции, включая:

  • диапазон значений каждой колонки партиции;
  • количество уникальных (distinct) значений;
  • дополнительные параметры.

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

ELT Pipelines


Потоки данных и слои их хранения и обработки в ManyChat выглядят примерно так:


Данные поступают в DWH из нескольких источников:

  • PHP-бэкенд события и изменения моделей данных;
  • Внешние API Intercom, Wistia, FaceBook и другие;
  • ManyChat Frontend события с фронтенда;
  • WebHooks сервисы, отдающие данные через вебхуки.

Давайте рассмотрим, как устроена эта схема, на примере события из бэкенда:

  1. PHP-бэкенд отправляет событие о создании нового аккаунта в ManyChat.
  2. Redis принимает данные и складывает в очередь.
  3. Отдельный python-процесс вычитывает эту очередь и сохраняет данные во временный JSON, загружая его в последующем в Snowflake.
  4. В Snowflake, с помощью python-ELT-процессов, мы прогоняем данные по всем необходимым слоям и, в итоге, раскладываем в Анкор-Модель.
  5. Аналитики используют DDS и SNP-слои с данными для сборки агрегированных витрин данных в слой DMA.

Аббревиатуры слоёв SA* расшифровываются как Staging Area for (Archive/Loading/Extract)

  • SNP слой для хранения агрегированных исторических данных из бэкэнд баз данных.
  • SAE слой для хранения сырых данных из Redis в виде одной колонки типа variant.
  • SAA слой для хранения обогащенных данных из Redis с добавлением служебных колонок с датами и id загрузки.
  • SAL более детальный слой данных с типизированными колонками. Таблицы в нем хранят только актуальные данные, при каждом запуске скрипта загрузки производится truncate table.
  • DDS 6 нф для хранения данных в виде 1 колонка SAL 1 таблица DDS.
  • DMA аналитический слой, в котором хранятся вьюхи, материализации и исследования аналитиков на базе DDS.

Статистика по объектам в схемах
Схема Количество объектов Количество представлений AVG строк (млн) AVG объём GB
SNP 3337 2 2 0.2
SAA 52 2 590 60
SAL 124 121 25 2.2
DDS 954 6 164 2.5
DMA 57 290 746 15

Используя 6 нф, DDS позволяет хранить достаточно большие объемы данных очень компактно. Все связи между сущностями осуществляются через целочисленные суррогатные ключи, которые отлично жмутся и очень быстро обрабатываются даже самым слабым XS-кластером.

SAA занимает более 80% объема хранилища из-за неструктурированных данных типа variant (сырой JSON). Раз в месяц SAA-слой скидывает данные в историческую схему.

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

Redis


http://personeltest.ru/aways/habrastorage.org/webt/ix/6m/a2/ix6ma2hvzmnxbfwkzhm_dc6ihl0.jpeg
В ManyChat активно используется Redis, и наш проект не стал исключением: он является шиной для обмена данными. Для быстрого и безболезненного старта в качестве языка написания ELT-движка был выбран python, а для хранения логов и статистики Postgres. Redis выступает в нашей архитектуре местом для временного хранения поступающей информации от всех источников. Данные в Redis хранятся в виде списка (List) JSON'ов.

http://personeltest.ru/aways/habrastorage.org/webt/6x/4k/52/6x4k52kwhxnrzj40geyblmoepfq.jpeg
Структура хранения данных в Redis

В каждом списке могут находится от 1 до N разнообразных моделей данных. Модели объединяются в списки методом дедукции. Например, все клики пользователей в независимости от источника кладутся в один список, но могут иметь разные модели данных (список полей).

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

Пример некоторых названий списков и моделей в нем:

  • EmailEvent (события происходящие с почтой)
    • email
    • email_package_reduce
  • SubscriberEvent (при создании или изменении подписчика, он появляется в этой очереди)
    • subscriber
  • ModelEvent (модели данных из бэкэнда и их события)
    • account_user
    • pro_subscription
    • wallet_top_up
    • И еще 100500 разных моделей
  • StaticDictionaries
    • Статичные словари из бэкенда. Информация о добавлении или изменении элемента словаря.

Весь ELT построен на python и использовании multiprocessing. Железо для всего ELT в ManyChat работает в AWS на m5.2xlarge инстансе:

  • 32 Гбайт RAM
  • Xeon Platinum 8175M CPU @ 2.50GHz

Первый подход


Первым подходом к построению ELT-процесса для нас стала простая загрузка данных, выполняющаяся в несколько шагов в одном скрипте по cron'у.

http://personeltest.ru/aways/habrastorage.org/webt/cx/zi/3q/cxzi3q-0qpk9lmxhaurtjdfi-9o.jpeg
Каждая очередь в Redis вычитывается своим собственным лоадером, запускаемым по расписанию в cron.

Первым этапом на рисунке выше является загрузка данных из очереди Redis в JSON-файл командой lpop(). Они вычитываются поэлементно из Redis, из каждой строки (словаря из JSON) снимается статистика по наполнению элементов словаря и затем записывается в Postgres. В этом же цикле данные записываются построчно в JSON-файл.

http://personeltest.ru/aways/habrastorage.org/webt/qu/6h/vo/qu6hvooizkjd5zvzzvwrmunwfho.png
Лоадеры для загрузки данных. Названия лоадеров совпадают с названиями загружаемых очередей.

Псевдокод цикла считывания данных из Redis в JSON:

batch_size = 1000000 # Количество элементов для считывания из очереди Rediswith open(json_file) as f:    while batch_size > 0:        row = redis.lpop('Model')        save_statistics(row)        batch_size -= 1        f.write(row)

Вся последующая загрузка данных поделена на этапы:

  1. Загрузка из JSON в SAE-слой;
  2. Обогащение и загрузка из SAE в SAA;
  3. Загрузка из SAA в заранее созданную структурированную таблицу в SAL-схеме;
  4. Загрузка данных из SAL в DDS схему.

Из плюсов такого подхода можно выделить:

  • Скорость адаптации. На внедрение нового сотрудника в пайплайн и процессы уходит 1 день.
  • Скорость реализации. Python позволяет делать практически что угодно с очень низким порогом входа.
  • Простота. При неисправности легко починить или запустить код руками.
  • Стоимость. Вся инфраструктура создана на уже существующих мощностях, из нового только Snowflake.

Конечно, были и минусы:

  • Определенные сложности с масштабированием. Если лоадер был настроен на считывание 1кк записей из Redis раз в 10 минут, а в очередь прилетело, например, 5кк событий, они считывались 50 минут. Бывали случаи, когда очередь не пустела в течении суток.

    Такие ситуации происходили крайне редко. Зная среднюю нагрузку наших сервисов, мы заранее выставляли более высокое ограничение на объем вычитываемых событий. А в случае внезапных увеличений объемов данных, производили загрузку руками с использованием более быстрого кластера и увеличенного количества вычитываемых объектов.
  • При любом вынужденном простое, тесте ELT-процессов или исправлении ошибок, мы останавливали загрузку из одной или нескольких очередей. Redis начинал наполняться бесконтрольно, и у нас могло закончиться место (30 Гбайт), что приводило к потере новых данных.
    http://personeltest.ru/aways/habrastorage.org/webt/az/pg/qe/azpgqey53ut27ilztvxajcye_nm.png
    Остановка загрузки одной из очередей в Redis могла привести к расходованию всей памяти и невозможности принимать данные
  • Скрипт загрузки данных (Loader) содержал полный цикл от Redis до DDS, и в случае поломки его приходилось запускать заново. Если ошибка произошла где-то посередине, например, потерялся только что записанный JSON-файл, восстановить его было проблематично. Помочь могла только infra-команда и выгрузка исторических данных за определенные даты к нам в шину. В других случаях инженерам приходилось комментировать код и запускать определенную часть скрипта вручную, контролируя загрузку данных.

Второй подход


Весь код наших интеграций был написан быстро и без оглядки на стандарты/практики. Мы запустили MVP, который показывал результат, но работать с ним не всегда было удобно. Именно поэтому мы решились на допиливание и переписывание нашего инструментария.

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

Мы произвели декомпозицию всего кода на несколько важных независимых частей.

  • Чтение данных из Redis. Загрузка данных из Redis должна быть максимально глупой: код выполняет только одну функцию, не затрагивая остальные компоненты системы.
  • Трансформация данных внутри Snowflake. Подразумевает загрузку данных из слоя SAA в SAL со сбором статистики, ведением истории загрузок и информированием в Slack о появлении новых моделей и / или полей в моделях.
  • Сборка DDS. Множество параллельно работающих процессов, загружающих данные.

Чтение данных из Redis


http://personeltest.ru/aways/habrastorage.org/webt/ca/uq/pa/cauqpayg8avputdrpbqrbrdk514.jpeg

RedisReader скрипт для непрерывного вычитывания шины Redis. Conf-файл для supervisord создан под каждую очередь и постоянно держит запущенным необходимый ридер.

Пример conf-файла для одной из очередей email_event:

[program:model_event_reader]command=/usr/bin/env python3 $DIRECTORY/RedisReader.py --queue='manychat:::model_event' --chunk_size=500000autostart=trueautorestart=truestopsignal=TERMstopwaitsecs=1800process_name=%(program_name)s_%(process_num)dnumprocs=4

Скрипт непрерывно мониторит определенную шину Redis, заданную через аргумент --queue на появление новых данных. Если данные в шине отсутствуют, он ждет RedisReader.IDLE_TIME секунд и повторно пробует прочитать данные. Если данные появились, скрипт считывает их через lpop() и складывает в файл вида /tmp/{queue_name}_pipe_{launch_id}_{chunk_launch_id}.json, где launch_id и chunk_launch_id сгенерированные уникальные int'ы. Когда количество строк в файле достигает уровня --chunk_size или заданное время --chunk_timeout истекло, RedisReader завершает запись файла и начинает его загрузку в Snowflake.

Полученные данные сперва параллельно загружаются в таблицы
SAE.{queue_name}_pipe_{launch_id}_{chunk_launch_id}, а затем в одном процессе вставляются простым insert'ом в таблицу SAA.{queue_name}_pipe не блокируя работу с уже существующими данными.

Все действия в RedisReader являются multiprocessing-safe и призваны сделать загрузку наиболее безопасной при одновременном использовании множества процессов для вычитки одной очереди Redis.

Устанавливая параметр numprocs, мы можем запускать столько RedisReader'ов, сколько требуется для своевременного вычитывания очереди.

После внедрения RedisReader исчезла проблема с неконтролируемым расходованием памяти Redis. При появлении в очереди, данные практически моментально считываются и складываются в Snowflake-слое SAA по следующим колонкам:

  • model название загружаемой модели данных
  • event_dt дата заливки данных
  • raw сами данные в JSON формате (variant)
  • launch_id внутренний сгенерированный номер загрузки

Трансформация данных внутри Snowflake


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

http://personeltest.ru/aways/habrastorage.org/webt/qb/8v/qx/qb8vqxfqzvhvustjxjxpdepfhao.jpeg

  1. На первом этапе необходимо получить список еще не обработанных launch_id. Для этого была создана специальная таблица engine.saa_to_sal_transfer, в которой хранится launch_id, статус его обработки is_done и прочая служебная информация. Задача скрипта взять то количество необработанных строчек по каждой модели, которое указано в параметрах загрузчика либо немного меньше.
  2. После этого по каждой модели собирается статистика. Мы храним данные о min / max значениях в колонке, типе данных, количестве ненулевых записей и множестве других вспомогательных характеристик. Сбор статистики является необязательным, для некоторых лоадеров, меняющихся крайне редко, сбор статистики отключен. При появлении новых полей (колонок) в статистике, инженеры увидят сообщение в Slack и приступят к созданию сущностей DDS для последующей загрузки.

    http://personeltest.ru/aways/habrastorage.org/webt/lb/-g/0h/lb-g0hjelxkkwapkl8gdr1qkjmi.png
    Часть таблицы статистики
  3. Далее происходит загрузка данных из слоя SAA в SAL. В SAL попадают только размеченные инженерами данные с описанием поля, правильным типом и названием, которые берутся из таблицы engine.sal_mapping
  4. Завершающий шаг трансформации UPDATE в engine.saa_to_sal_transfer для проставления статуса is_done, если загрузка в SAL прошла успешно.

Сборка DDS


http://personeltest.ru/aways/habrastorage.org/webt/ip/ic/8s/ipic8sjalyvmbrhq34zihe7rlw8.jpeg

Сборка таблиц для слоя DDS происходит на основе данных из SAL-схемы. Она изменилась меньше всего с момента первой реализации. Мы добавили полезные фичи: выбор типа отслеживания изменений данных (Slowly Changing Dimension) в виде SCD1 / SCD0, а также более быстрые неблокирующие вставки в таблицы.

Данные в каждую таблицу в DDS-слое загружаются отдельным процессом. Это позволяет параллельно работать со множеством таблиц и не тратить время на последовательную обработку данных.

Загрузка в DDS разделена на 2 этапа:

  1. Сначала грузятся сущности для формирования суррогатного ключа;
  2. Затем загружаются атрибуты и отношения.

Загрузка сущностей


Загрузка сущностей подразумевает загрузку только уникальных значений в таблицы типа DDS.E_{EntityName}, где EntityName название загружаемой сущности.

self.entity_loader(entity_name: str, source_schema: str, id_source_table_list: list),

Метод загрузки принимает в качестве атрибутов название сущности, схему исходных данных, а также массив из названия колонки в SAL-таблице и самого названия исходной SAL-таблицы. Внутри происходит либо обычный MERGE INTO, либо INSERT FIRST.

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

Загрузка Отношений и Атрибутов


Загрузка отношений и атрибутов реализована похожим образом, единственное отличие при вставке данных в DDS-схему происходит больше join'ов и проверок данных на актуальность.

Атрибуты:

self.attribute_loader(entity: str, attribute: str, source_table: str, id_column: str, value_column: str, historicity: str)

Отношения:

self.relation_loader(left_entity: str, right_entity: str, source_table: str, left_id: str, right_id: str, historicity: str)

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

Псевдокод одного из лоадеров
 from Loaders.SnowflakeLoaders import SnowflakeLoader    class ModelEventLoader(SnowflakeLoader):        DEFAULT_SOURCE_TABLE = 'saa.model_pipe'        DEFAULT_BATCH_STAT_SAMPLE_PERCENT = 50        DEFAULT_BATCH_SIZE = 1000000        DEFAULT_BUS_FULFILMENT_THRESHOLD = 100000        DEFAULT_HOURS_PASSED_THRESHOLD = 1.0        def sal_to_dds(self):            loaders = [                self.entity_loader('Account', 'sal', ['page_id', 'rb_model_event']),                self.entity_loader('Subscriber', 'sal', ['subscriber_id', 'rb_model_event']),                self.entity_loader('Device', 'sal', ['device_id', 'rb_model_event']),            ]            self.run_loaders(loaders)            loaders = [                self.attribute_loader('Account', 'IsActive', 'sal.rb_model_event', 'page_id', 'is_active', historicity='scd1'),                self.attribute_loader('Subscriber', 'Name', 'sal.rb_model_event', 'subscriber_id', 'name', historicity='scd1'),                self.attribute_loader('Device', 'Platform', 'sal.rb_model_event', 'device_id', 'platform', historicity='scd0'),                self.relation_loader('Subscriber', 'Account', 'sal.rb_model_event', 'subscriber_id', 'account_id', historicity='scd1'),                self.relation_loader('Subscriber', 'Device', 'sal.rb_model_event', 'subscriber_id', 'device_id', historicity='scd0'),            ]            self.run_loaders(loaders)        def run(self):            self.truncate_sal('rb_model_event')            self.saa_to_sal()            self.run_sal_to_dds()    if __name__ == '__main__':        ModelEventLoader().do_ELT()


Лоадер каждый раз проверяет условия запуска. Если они заданы, и необработанных данных в SAA-слое накопилось больше чем DEFAULT_BUS_FULFILMENT_THRESHOLD или после последнего запуска прошло больше чем DEFAULT_HOURS_PASSED_THRESHOLD часа, то будет взято не более DEFAULT_BATCH_SIZE строк из SAA-таблицы DEFAULT_SOURCE_TABLE, а также собрано статистики по DEFAULT_BATCH_STAT_SAMPLE_PERCENT процентам данных.

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

RedisReader в свою очередь работает независимо от всей остальной системы, ежесекундно опрашивая очереди в Redis. Загрузка данных SAA SAL и далее в DDS тоже может работать абсолютно независимо, но запускается в одном скрипте.

Так мы смогли избавиться от прежних проблем:

  • Затирание JSON файла с данными.
  • Переполнение памяти Redis при остановке лоадеров (теперь можно останавливать на сколько угодно, данные уже будут в Snowflake в SAA-слое).
  • Ручное комментирование кода и запуск скриптов загрузки.

Сейчас на постоянной основе мы загружаем данные из 26 очередей в Redis. Как только данные появляются в них, они сразу попадают в SAA-слой и ждут своей очереди на обработку и доведения до DDS. В среднем мы получаем 1400 событий в секунду в диапазоне от 100 до 5000 в зависимости от времени суток и сезонности.

image
Количество полученных данных. Каждый цвет отвечает за отдельный поток данных.

Заключение


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

При этом было реализовано множество сторонних процессов, например Data Quality, Data Governance и материализация представлений.

Фактически добавление нового лоадера теперь сводится к заполнению полей в Google Sheet и построению модели будущих таблиц в схеме DDS.

Про нюансы работы наших ELT-процессов или аспекты работы со Snowflake спрашивайте меня в комментариях обязательно отвечу.

Полезные ссылки


Подробнее..

Кому с Redux жить хорошо

11.02.2021 14:07:02 | Автор: admin
Приветствую всех любителей хорошей инженерки! Меня зовут Евгений Иваха, я фронтенд-разработчик в команде, занимающейся дев-программой в ManyChat. В рамках дев-программы мы разрабатываем инструменты, позволяющие расширять функциональность ManyChat за счет интеграции со сторонними системами.

Существует мнение, что разработка через тестирование, или по канонам Test Driven Development (TDD) для фронтенда не применима. В данной статье я постараюсь развенчать этот миф и покажу, что это не только возможно, но и очень удобно и приятно.

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



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

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

Поскольку, мы в ManyChat в начале 2020 года полностью перешли на TypeScript, будем писать код сразу с использованием строгой типизации.

Redux


Что такое Redux? Redux это паттерн и библиотека для управления и обновления состоянием приложения с использованием специальных событий, называемых Action. Он предоставляет централизованное хранилище состояния, которое используется во всём приложении с правилами, гарантирующими предсказуемое изменение этого состояния. Если посмотреть на диаграмму потока данных в Redux для приложений на React, мы увидим примерно следующее:



При необходимости изменения состояния, например, при клике на элемент в DOM, вызывается Action creator, который создаёт определенный Action. Этот Action c помощью метода Dispatch отправляется в Store, где он передаётся на обработку в Reducers. Редьюсеры, в свою очередь, на основании текущего состояния и информации, которая находится в экшене, возвращают новое состояние приложения, которое принимает React с помощью Selectors для нового рендера DOM. Более подробно о каждом компоненте Redux будет рассказано ниже по ходу разработки приложения.

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

Задача


Создадим форму принятия какого-то пользовательского соглашения. Форма должна содержать чекбокс, который следует отметить пользователем, в случае, если он принимает соглашение, а так же кнопку Submit, которая становится активной только при активации чекбокса. Выглядеть это должно примерно вот так:



Воспользуемся шаблоном create-react-app:

npx create-react-app my-app --template typescriptcd my-appnpm start

Запустили, убедились, что приложение работает.



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

Теперь приступим к самому вкусному подключению Redux в наш проект. Т.к. приложение у нас совершенно пустое, мы не знаем структуру стора, оставим его напоследок.

Установим нужные пакеты:
npm i redux react-redux redux-mock-store @types/redux @types/react-redux @types/redux-mock-store  

Actions


Что такое Action? Это обычный Javascript объект, у которого есть обязательное свойство type, в котором содержится, как правило, осознанное имя экшена. Создатели Redux рекомендуют формировать строку для свойства type по шаблону домен/событие. Также в нём может присутствовать дополнительная информация, которая, обычно, складывается в свойство payload. Экшены создаются с помощью Action Creators функций, которые возвращают экшены.

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

Напишем первый тест. Для тестирования используем уже ставший стандартным фреймворк Jest. Для запуска тестов в следящем режиме, достаточно в корне проекта выполнить команду npm test.
// actions/actions.test.tsimport { checkboxClick } from '.'describe('checkboxClick', () => {  it('returns checkboxClick action with action name in payload', () => {    const checkboxName = 'anyCheckbox'    const result = checkboxClick(checkboxName)    expect(result).toEqual({ type: 'checkbox/click', payload: checkboxName })  })})

Здесь мы проверяем, Action Creator вернёт экшн с нужным типом и правильными данными, а именно с названием чекбокса. И больше нам здесь нечего проверять.

Само собой, тест у нас красный (сломанный), т.к. код ещё не написан:



Пора написать код:
// actions/package.json{  "main": "./actions"}// actions/actions.tsexport const checkboxClick = (name: string) => ({ type: 'checkbox/click', payload: name })

Проверяем:



Тест пройден, можем приступить к рефакторингу. Здесь мы видим явное дублирование константы с типом экшена, вынесем её в отдельный модуль.
// actionTypes.tsexport const CHECKBOX_CLICK = 'checkbox/click'

Поправим тест:
// actions/actions.test.tsimport { CHECKBOX_CLICK } from 'actionTypes'import { checkboxClick } from '.'describe('checkboxClick', () => {  it('returns checkboxClick action with action name in payload', () => {    const checkboxName = 'anyCheckbox'    const result = checkboxClick(checkboxName)    expect(result).toEqual({ type: CHECKBOX_CLICK, payload: checkboxName })  })})

Тест не проходит, потому что мы не использовали относительный путь к actionTypes. Чтобы это исправить, добавим в tsconfig.json в секцию compilerOptions следующий параметр "baseUrl": "src". После этого понадобится перезапустить тесты вручную.

Убедимся, что тест позеленел, теперь поправим сам код:
// actions/actions.tsimport { CHECKBOX_CLICK } from 'actionTypes'export const checkboxClick = (name: string) => ({ type: CHECKBOX_CLICK, payload: name })

Ещё раз убеждаемся, что тест проходит, и можем двигаться дальше.

Reducers


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

Хранить состояние чекбоксов (отмечены они или нет) мы будем простым объектом, где ключом будет выступать название чекбокса, а в булевом значении непосредственно его состояние.
{  checkboxName: true}

Приступим. Первый тест будет проверять, что мы получаем исходное состояние, т.е. пустой объект.
// reducers/reducers.test.tsimport { checkboxReducer } from '.'describe('checkboxReducer', () => {  it('creates default state', () => {    const state = checkboxReducer(undefined, { type: 'anyAction' })    expect(state).toEqual({})  })})

Т.к. у нас даже нет файла с редьюсером, тест сломан. Напишем код.
// reducers/package.json{  "main": "./reducers"}// reducers/reducers.tsconst initialState: Record<string, boolean> = {}export const checkboxReducer = (state = initialState, action: { type: string }) => {  return state}

Первый тест редьюсера починили, можем написать новый, который уже проверит, что получим в результате обработки экшена с информацией о нажатом чекбоксе.
// reducers/reducers.test.tsimport { CHECKBOX_CLICK } from 'actionTypes'import { checkboxReducer } from '.'describe('checkboxReducer', () => {  it('creates default state', () => {    const state = checkboxReducer(undefined, { type: 'anyAction' })    expect(state).toEqual({})  })  it('sets checked flag', () => {    const state = checkboxReducer(undefined, { type: CHECKBOX_CLICK, payload: 'anyName' })    expect(state.anyName).toBe(true)  })})

Минимальный код для прохождения данного теста будет выглядеть следующим образом:
// reducers/reducers.tsimport { CHECKBOX_CLICK } from 'actionTypes'const initialState: Record<string, boolean> = {}export const checkboxReducer = (  state = initialState,  action: { type: string; payload?: string },) => {  if (action.type === CHECKBOX_CLICK && action.payload) {    return { ...state, [action.payload]: true }  }  return state}

Мы убедились, что при обработке экшена, в котором содержится имя чекбокса, в state будет состояние о том, что он отмечен. Теперь напишем тест, который проверит обратное поведение, т.е. если чекбокс был отмечен, то отметка должна быть снята, свойство должно получить значение false.
// reducers/reducers.test.ts  it('sets checked flag to false when it was checked', () => {    const state = checkboxReducer({ anyName: true }, { type: CHECKBOX_CLICK, payload: 'anyName' })    expect(state.anyName).toBe(false)  })

Убеждаемся, что тест красный, т.к. у нас всегда устанавливается значение в true, ведь до сего момента у нас не было других требований к коду. Исправим это.
// reducers/reducers.tsimport { CHECKBOX_CLICK } from 'actionTypes'const initialState: Record<string, boolean> = {}export const checkboxReducer = (  state = initialState,  action: { type: string; payload?: string },) => {  if (action.type === CHECKBOX_CLICK && action.payload) {    return { ...state, [action.payload]: !state[action.payload] }  }  return state}

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

Selectors


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

Напишем первый тест для селектора.
// selectors/selectors.test.tsimport { getCheckboxState } from './selectors'describe('getCheckboxState', () => {  const state = {    checkboxes: { anyName: true },  }  it('returns current checkbox state', () => {    const result = getCheckboxState('anyName')(state)    expect(result).toBe(true)  })})

Теперь заставим его позеленеть.

Так как селектор должен знать, откуда извлекать информацию, определим структуру хранения.
// types.tsexport type State = {  checkboxes: Record<string, boolean>}

Теперь напишем код селектора. Здесь используется функция высшего порядка из-за особенностей хука useSelector пакета react-redux, который принимает на вход функцию, принимающую один аргумент текущее состояние стора, а нам требуется сообщить ещё дополнительные параметры название чекбокса.
// selectors/package.json{  "main": "./selectors"}// selectors/selectors.tsimport { State } from 'types'export const getCheckboxState = (name: string) => (state: State) => state.checkboxes[name]

Кажется, мы всё сделали правильно, тест теперь зелёный. Но что произойдёт, если у нас ещё не было информации о состоянии чекбокса? Напишем ещё один тест.
// selectors/selectors.test.ts  it('returns false when checkbox state is undefined', () => {    const result = getCheckboxState('anotherName')(state)    expect(result).toBe(false)  })

Получим вот такую картину:



И это правильно, мы получили на выходе undefined, т.е. state ничего не знает об этом чекбоксе. Исправим код.
// selectors/selectors.tsimport { State } from 'types'export const getCheckboxState = (name: string) => (state: State) => state.checkboxes[name] ?? false

Вот теперь селектор работает, как и требуется.

Store


Давайте теперь создадим сам Store, т.е. специальный объект Redux, в котором хранится состояние приложения.
// store.tsimport { AnyAction, createStore, combineReducers } from 'redux'import { State } from 'types'import { checkboxReducer } from 'reducers'export const createAppStore = (initialState?: State) =>  createStore<State, AnyAction, unknown, unknown>(    combineReducers({      checkboxes: checkboxReducer,    }),    initialState,  )export default createAppStore()

Этот код мы отдельно тестировать не будем, т.к. мы используем стороннюю библиотеку, которая уже протестирована. Но далее мы напишем интеграционный тест, который проверит всю связку React + Redux.

React Components


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

Для более удобной работы мы написали небольшую утилиту для тестов. В ней несколько больше функциональности, чем требуется для нашего первого теста, но далее мы всё это применим. Используем удобную библиотеку react-test-renderer, которая позволяет не производить рендер в настоящий DOM, а получать его JS представление. Установим пакет:
npm i react-test-renderer @types/react-test-renderer

Приступим к написанию тестов на компоненты. Начнём непосредственно с чекбокса.

Checkbox


// components/Checkbox/Checkbox.test.tsximport { create } from 'react-test-renderer'import Checkbox from '.'describe('Checkbox', () => {  it('renders checkbox input', () => {    const checkboxName = 'anyName'    const renderer = create(<Checkbox />)    const element = renderer.root.findByType('input')    expect(element.props.type).toBe('checkbox')  })})

Первый тест компонента проверяет, что внутри Checkbox рендерится стандартный input с типом checkbox.

Сделаем тест зелёным.
// components/Checkbox/package.json{  "main": "Checkbox"}// components/Checkbox/Checkbox.tsximport React from 'react'const Checkbox: React.FC = () => {  return (    <div>      <input type="checkbox" />    </div>  )}export default Checkbox

Отлично, теперь добавим свойство label, содержащее текст для html элемента label, который должен отображаться рядом с чекбоксом.
// components/Checkbox/Checkbox.test.tsxit('renders label', () => {    const labelText = 'anyLabel'    const renderer = create(<Checkbox label={labelText} />)    const element = renderer.root.findByType('label')    expect(element.props.children).toBe(labelText)  })

Заставим тест пройти.
// components/Checkbox/Checkbox.tsxconst Checkbox: React.FC<{ label: string }> = ({ label }) => {  return (    <div>      <input type="checkbox" />      <label>{label}</label>    </div>  )}

Осталась небольшая деталь чекбокс как-то должен себя идентифицировать, кроме того, для корректной работы клика по label, нужно прописать id чекбокса в свойство htmlFor. Напишем тест, проверяющий установку свойства id:
// components/Checkbox/Checkbox.test.tsx  it('sets name prop as input id', () => {    const checkboxName = 'anyCheckbox'    const renderer = create(<Checkbox name={checkboxName} label={'anyLabel'} />)    const element = renderer.root.findByType('input')    expect(element.props.id).toBe(checkboxName)  })

Убедившись, что он красный, исправим код:
// components/Checkbox/Checkbox.tsxconst Checkbox: React.FC<{ name: string; label: string }> = ({ name, label }) => {  return (    <div>      <input id={name} type="checkbox" />      <label>{label}</label>    </div>  )}

Тест зеленый, можем написать ещё один, который проверит установку свойства name в свойство htmlFor элемента label.
// components/Checkbox/Checkbox.test.tsx  it('sets name prop as label htmlFor', () => {    const checkboxName = 'anyCheckbox'    const renderer = create(<Checkbox name={checkboxName} label={'anyLabel'} />)    const element = renderer.root.findByType('label')    expect(element.props.htmlFor).toBe(checkboxName)  })

Тест красный, нужно снова поправить код.
// components/Checkbox/Checkbox.tsxconst Checkbox: React.FC<{ name: string; label: string }> = ({ name, label }) => {  return (    <div>      <input id={name} type="checkbox" />      <label htmlFor={name}>{label}</label>    </div>  )}

Пора бы подключить Store к компоненту. Напишем тест, который покажет, что состояние чекбокса (свойство checked) соответствует тому, что хранится в Store.
// components/Checkbox/Checkbox.test.tsximport { Provider } from 'react-redux'import { create } from 'react-test-renderer'import { createAppStore } from 'store'import Checkbox from '.'// omit old code  it('sets checked flag from store when it`s checked', () => {    const store = createAppStore({ checkboxes: { anyName: true } })    const renderer = create(      <Provider store={store}>        <Checkbox name="anyName" label="anyLabel" />      </Provider>,    )    const element = renderer.root.findByType('input')    expect(element.props.checked).toBe(true)  })

Тест пока красный, т.к. компонент ничего не знает о сторе. Заставим тест позеленеть.
// components/Checkbox/Checkbox.tsximport React from 'react'import { useSelector } from 'react-redux'import { getCheckboxState } from 'selectors'const Checkbox: React.FC<{ name: string; label: string }> = ({ name, label }) => {  const checked = useSelector(getCheckboxState(name))  return (    <div>      <input id={name} type="checkbox" checked={checked} />      <label htmlFor={name}>{label}</label>    </div>  )}export default Checkbox

Тест пройден. Наконец-то, мы задействовали Redux! Мы использовали ранее написанный селектор getCheckboxState, который вызвали с помощью хука useSelector, получили значение и передали его в свойство checked элемента input. Но сейчас произошла другая проблема сломались остальные тесты на компонент.



Дело в том, что ранее в тестах мы не передавали стор в компонент. Выделим часть с провайдером стора в функцию и перепишем наши тесты.
// components/Checkbox/Checkbox.test.tsximport { ReactElement } from 'react'import { Provider } from 'react-redux'import { create } from 'react-test-renderer'import { createAppStore } from 'store'import { State } from 'types'import Checkbox from '.'export const renderWithRedux = (node: ReactElement, initialState: State = { checkboxes: {} }) => {  const store = createAppStore(initialState)  return create(<Provider store={store}>{node}</Provider>)}describe('Checkbox', () => {  it('renders checkbox input', () => {    const checkboxName = 'anyName'    const renderer = renderWithRedux(<Checkbox />)    const element = renderer.root.findByType('input')    expect(element.props.type).toBe('checkbox')  })  it('renders label', () => {    const labelText = 'anyLabel'    const renderer = renderWithRedux(<Checkbox label={labelText} />)    const element = renderer.root.findByType('label')    expect(element.props.children).toBe(labelText)  })  it('sets name prop as input id', () => {    const checkboxName = 'anyCheckbox'    const renderer = renderWithRedux(<Checkbox name={checkboxName} label={'anyLabel'} />)    const element = renderer.root.findByType('input')    expect(element.props.id).toBe(checkboxName)  })  it('sets name prop as label htmlFor', () => {    const checkboxName = 'anyCheckbox'    const renderer = renderWithRedux(<Checkbox name={checkboxName} label={'anyLabel'} />)    const element = renderer.root.findByType('label')    expect(element.props.htmlFor).toBe(checkboxName)  })  it('sets checked flag from store when it`s checked', () => {    const initialState = { checkboxes: { anyName: true } }    const renderer = renderWithRedux(<Checkbox name="anyName" label="anyLabel" />, initialState)    const element = renderer.root.findByType('input')    expect(element.props.checked).toBe(true)  })})

Функция renderWithRedux выглядит достаточно полезной, вынесем её в отдельный модуль и импортируем в тестах.
// utils.tsximport { ReactElement } from 'react'import { Provider } from 'react-redux'import { create } from 'react-test-renderer'import { Store } from './types'import { createAppStore } from './store'export const renderWithRedux = (node: ReactElement, initialState: Store = { checkboxes: {} }) => {  const store = createAppStore(initialState)  return create(<Provider store={store}>{node}</Provider>)}

В итоге, шапка тестового файла будет выглядеть вот так:
// components/Checkbox/Checkbox.test.tsximport { renderWithRedux } from 'utils'import Checkbox from '.'describe('Checkbox', () => {

Для полной уверенности напишем ещё один тест, который проверит, что checked бывает и false.
// components/Checkbox/Checkbox.test.tsx  it('sets checked flag from store when it`s unchecked', () => {    const initialState = { checkboxes: { anyName: false } }    const renderer = renderWithRedux(<Checkbox name="anyName" label="anyLabel" />, initialState)    const element = renderer.root.findByType('input')    expect(element.props.checked).toBe(false)  })

Тест пройден, но у нас теперь появилось два теста с похожими описаниями и почти идентичным кодом, давайте немного модифицируем наши тесты, создав табличный тест. Последние два теста превратятся в один:
// components/Checkbox/Checkbox.test.tsx  test.each`    storedState | state    ${true}     | ${'checked'}    ${false}    | ${'unchecked'}  `('sets checked flag from store when it`s $state', ({ storedState }) => {    const initialState = { checkboxes: { anyName: storedState } }    const renderer = renderWithRedux(<Checkbox name="anyName" label="anyLabel" />, initialState)    const element = renderer.root.findByType('input')    expect(element.props.checked).toBe(storedState)  })

Так уже лучше. А теперь самое вкусное напишем интеграционный тест, который проверит, что при нажатии на чекбокс, он изменит своё состояние, т.е. свойство checked.
// components/Checkbox/Checkbox.test.tsximport { act } from 'react-test-renderer'// omit old code    it('changes it`s checked state when it`s clicked', () => {    const initialState = { checkboxes: { anyName: false } }    const renderer = renderWithRedux(<Checkbox name="anyName" label="anyLabel" />, initialState)    const element = renderer.root.findByType('input')    act(() => {      element.props.onChange()    })    expect(element.props.checked).toBe(true)  })

Здесь мы воспользовались функцией act, пакета react-test-renderer, выполняя которую, мы убеждаемся в том, что все сайд-эффекты уже произошли и мы можем продолжить проверки. И далее проверяем, что когда будет вызвано событие onChange на нашем чекбоксе, он изменит свойство checked на true. Пока этого не происходит, требуется написать код. Окончательный вариант компонента примет вот такой вид.
// components/Checkbox/Checkbox.tsximport React from 'react'import { useDispatch, useSelector } from 'react-redux'import { getCheckboxState } from 'selectors'import { checkboxClick } from 'actions'const Checkbox: React.FC<{ name: string; label: string }> = ({ name, label }) => {  const dispatch = useDispatch()  const checked = useSelector(getCheckboxState(name))  const handleClick = React.useCallback(() => {    dispatch(checkboxClick(name))  }, [dispatch, name])  return (    <div>      <input id={name} type="checkbox" checked={checked} onChange={handleClick} />      <label htmlFor={name}>{label}</label>    </div>  )}export default Checkbox

В коде мы навесили обработчик на событие change, который отправляет action в store, создаваемый функцией checkboxClick. Как видим, тест позеленел. Не открывая браузера и даже не запуская сборку приложения, мы имеем протестированный компонент с отдельным слоем бизнес-логики, заключенной в Redux.

AgreementSubmitButton


Нам требуется ещё один компонент непосредственно кнопка Submit, создадим его. Конечно, вначале тест:
// components/AgreementSubmitButton/AgreementSubmitButton.test.tsximport { renderWithRedux } from 'utils'import AgreementSubmitButton from '.'describe('AgreementSubmitButton', () => {  it('renders button with label Submit', () => {    const renderer = renderWithRedux(<AgreementSubmitButton />)    const element = renderer.root.findByType('input')    expect(element.props.type).toBe('button')    expect(element.props.value).toBe('Submit')  })})

Теперь заставим тест позеленеть:
// components/AgreementSubmitButton/package.json{  "main": "./AgreementSubmitButton"}// components/AgreementSubmitButton/AgreementSubmitButton.tsximport React from 'react'const AgreementSubmitButton: React.FC = () => {  return <input type="button" value="Submit" />}export default AgreementSubmitButton

Тест зелёный, начало положено. Напишем новый тест, проверяющий зависимость свойства disabled новой кнопки от состояния чекбокса. Т.к. может быть два состояния, вновь используем табличный тест:
// components/AgreementSubmitButton/AgreementSubmitButton.test.tsx  test.each`    checkboxState | disabled | agreementState    ${false}      | ${true}  | ${'not agreed'}    ${true}       | ${false} | ${'agreed'}  `(    'render button with disabled=$disabled when agreement is $agreementState',    ({ checkboxState, disabled }) => {      const initialState = { checkboxes: { agree: checkboxState } }      const renderer = renderWithRedux(<AgreementSubmitButton />, initialState)      const element = renderer.root.findByType('input')      expect(element.props.disabled).toBe(disabled)    },  )

Имеем двойной красный тест, напишем код для прохождения этого теста. Компонент станет выглядеть вот так:
// components/AgreementSubmitButton/AgreementSubmitButton.tsximport React from 'react'import { useSelector } from 'react-redux'import { getCheckboxState } from 'selectors/selectors'const AgreementSubmitButton: React.FC = () => {  const checkboxName = 'agree'  const agreed = useSelector(getCheckboxState(checkboxName))  return <input type="button" value="Submit" disabled={!agreed} />}export default AgreementSubmitButton

Ура, все тесты зелёные!
Следует обратить внимание, что в табличном тесте мы намеренно использовали два различных параметра checkboxState и disabled, хотя может показаться, что достаточно только первого, а в тесте написать вот так expect(element.props.disabled).toBe(!disabled). Но это плохой паттерн закладывать какую-то логику внутри тестов. Вместо этого мы явно описываем входные и выходные параметры. Так же, мы здесь немного ускорились, т.к., фактически написали два теста за раз. Такое допустимо, когда чувствуешь в себе силы и понимаешь, что реализация достаточно очевидная. Когда уровень владения TDD ещё не совершенный, лучше создавать по одному тесту за раз. В нашем случае это писать по одной строчке в таблице.

LicenseAgreement


Оформим нашу работу в то, ради чего мы всё это затевали в форму принятия лицензионного соглашения. Какие имеются требования к форме:
  1. Содержится заголовок и непосредственно текст лицензионного соглашения. Эта часть компонента не требует тестирования.
  2. На форме имеется компонент Checkbox с определенными label и name. Это можно и нужно тестировать.
  3. На форме имеется кнопка AgreementSubmitButton. Это тоже прекрасно поддаётся тестированию.

Приступим, первый тест на то, что на форме есть Checkbox:
// components/LicenseAgreement/LicenseAgreement.test.tsximport { renderWithRedux } from 'utils'import Checkbox from 'components/Checkbox'import LicenseAgreement from '.'jest.mock('components/Checkbox', () => () => null)describe('LicenseAgreement', () => {  it('renders Checkbox with name and label', () => {    const renderer = renderWithRedux(<LicenseAgreement />)    const element = renderer.root.findByType(Checkbox)    expect(element.props.name).toBe('agree')    expect(element.props.label).toBe('Agree')  })})

На что тут стоит обратить внимание мы использовали тестовый дублёр для компонента Checkbox в строчке jest.mock('components/Checkbox', () => () => null). Это делает наш тест изолированным, таким образом он не зависит от реализации Checkbox, возможные ошибки в этом компоненте не повлияют на результат выполнения данного теста. Дополнительно это экономит вычислительные ресурсы и время выполнения тестов. Тест красный, требуется написать правильный код:
// components/LicenseAgreement/package.json{  "main": "./LicenseAgreement"}// src/components/LicenseAgreement/LicenseAgreement.tsximport React from 'react'import Checkbox from 'components/Checkbox'const LicenseAgreement: React.FC = () => {  return (    <div>      <Checkbox name="agree" label="Agree" />    </div>  )}export default LicenseAgreement

Получили зеленый тест, можем написать второй для этого компонента. Файл с тестами изменится:
// components/LicenseAgreement/LicenseAgreement.test.tsximport { renderWithRedux } from 'utils'import Checkbox from 'components/Checkbox'import AgreementSubmitButton from 'components/AgreementSubmitButton'import LicenseAgreement from '.'jest.mock('components/Checkbox', () => () => null)jest.mock('components/AgreementSubmitButton', () => () => null)describe('LicenseAgreement', () => {  it('renders Checkbox with name and label', () => {    const renderer = renderWithRedux(<LicenseAgreement />)    const element = renderer.root.findByType(Checkbox)    expect(element.props.name).toBe('agree')    expect(element.props.label).toBe('Agree')  })  it('renders SubmitAgreementButton', () => {    const renderer = renderWithRedux(<LicenseAgreement />)    expect(() => renderer.root.findByType(AgreementSubmitButton)).not.toThrow()  })})

Чтобы он позеленел, добавим AgreementSubmitButton в компонент:
// src/components/LicenseAgreement/LicenseAgreement.tsximport React from 'react'import Checkbox from 'components/Checkbox'import AgreementSubmitButton from 'components/AgreementSubmitButton'const LicenseAgreement: React.FC = () => {  return (    <div>      <Checkbox name="agree" label="Agree" />      <AgreementSubmitButton />    </div>  )}export default LicenseAgreement

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

Ключ на старт!


Вставим над компонентами сам текст соглашения, далее можем добавлять компонент в приложение. В сгенерированном приложении имеется корневой компонент App, модифицируем его тесты на проверку рендера LicenseAgreement:
// App.test.tsximport { renderWithRedux } from 'utils'import LicenseAgreement from 'components/LicenseAgreement'import App from 'App'jest.mock('components/LicenseAgreement', () => () => null)test('renders LicenseAgreement', () => {  const renderer = renderWithRedux(<App />)  expect(() => renderer.root.findByType(LicenseAgreement)).not.toThrow()})

Заставим тест позеленеть:
// App.tsximport React from 'react'import LicenseAgreement from 'components/LicenseAgreement'const App: React.FC = () => {  return <LicenseAgreement />}export default App

Мы получили зелёный тест, можно, наконец, запустить сборку приложения с помощью npm start. Сборка пройдёт успешно, но в браузере мы увидим следующую картину:



Это говорит о том, что мы не подключили Redux store в само приложение. Сделаем это в файле index.tsx:
// index.tsximport React from 'react'import ReactDOM from 'react-dom'import { Provider } from 'react-redux'import 'index.css'import store from 'store'import App from 'App'ReactDOM.render(  <React.StrictMode>    <Provider store={store}>      <App />    </Provider>  </React.StrictMode>,  document.getElementById('root'),)

Теперь приложение запускается, всё работает, как ожидается, кроме внешнего вида:



Исправим это, поправив вёрстку, и получим конечный результат:



Заключение


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

Во второй части данной статьи предполагался рассказ о библиотеке Redux Tookilt, которая значительно упрощает использование Redux в разработке фронтенд-приложений, но я решил в следующей статье показать, как можно написать настоящее полезное приложение, хоть и очень простое, на React, Redux и Redux Toolkit.

Исходные коды полученного приложения доступны на GitHub.

Дополнительные источники информации:
Подробнее..

22 сентября, Онлайн-митап Product Engineering Meetup 2 Культура разработки

17.09.2020 16:18:03 | Автор: admin
22 сентября мы проводим онлайн-митап Product Engineering Meetup #2 Культура разработки в продуктовых компаниях.

В прошлом году мы собирались, чтобы поговорить о том, кто такой продуктовый инженер, зачем разработчику развивать продуктовое мышление, как найти баланс между продуктом и разработкой (записи можно найти тут).

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

Спикеры инженеры и менеджеры продукта из Badoo, ManyChat, Додо Пицца и Работа.ру. Будем говорить о том, какие практики помогают командам создавать качественные продукты. Обсудим, какие процессы разработки помогают фокусироваться на донесении ценности пользователям и соблюдать сроки. Рассмотрим, как находить компромисс между бизнесом и разработкой и определять границы ответственности.

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

Регистрация доступна по ссылке, а подробности программы читайте под катом.



Доклады


История эволюции фичи: от MVP до одной из основных функциональностей продукта Евгений Жуков, Backend Developer (ManyChat)

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

Как перестать беспокоиться и начать верить А/Б тестам 2 Максим Кислов, Frontend Engineering Manager (Badoo)

После того как вы победили А/Б тесты и интегрировали их в свое рабочее окружение, кажется, что все позади. На самом деле приключение только начинается: у команды нет накопленной экспертизы, процессы не налажены, а бизнес хочет полагаться на А/Б тестирование для принятия ключевых решений и ожидает результатов. Я расскажу:

  • как организовать работу с А/Б тестами внутри компании;
  • как дать бизнесу ответ на вопрос ну как там наш тест?;
  • как определить границу ответсвенности техлида и разделить ответственность с продактом.

Инициатива не наказуема. Почему разработчика важно погружать в продуктовую жизнь Лаша Харчилава, Ведущий Менеджер Продукта (Работа.ру)

  • Что нужно сделать в первый день новому продакту, чтобы жизнь разработчика стала лучше.
  • Что такое продуктовый онбординг и зачем он разработчику.
  • Для чего нужны разработчики в краткосрочном и среднесрочном планировании продуктовых задач.
  • План к действию: есть проблема сначала спроси у разработчика.
  • Какая связь между разработчиками и результатами АБ-теста.

Как релиз продукта вышел на полгода позже, а пересмотр процессов разработки научил нас попадать в сроки Борис Герн, B2C Product Lead (Додо Пицца)

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

У каждой команды в Додо свой подход в построении процессов, важно как можно раньше найти свой, иначе компании будет сложно масштабироваться.

В презентации рассказывается о том, как команда одного из наших продуктов приходила к своему рабочему процессу:

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

Участие


Встреча пройдёт онлайн, 22 сентября. Начало в 19:00.

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

Для участия нужно заполнить форму регистрации, чтобы получить ссылку для подключения и напоминание о трансляции.
Подробнее..

Категории

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

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru