Главная » Хабрахабр » Конечные React Компоненты

Конечные React Компоненты

Различные авторы пишут различные статьи в поддержку существующего порядка и обьясняют почему все "правильно", так что всем понятно — партия держит правильный курс. Чем мне нравится экосистема React, так это тем, что за многими решениями сидит ИДЕЯ.

Через некоторые время ИДЕЯ немного меняется, и все начинается с начала.

А начало этой истории — разделение компонент на Контейнеры и неКонтейнеры (в народе — Тупые Компоненты, простите за мой франзуский).

Проблема

В последнее время есть некоторое движение в сторону integrations tests — ну вы знаете "Write tests. Проблема очень проста — юнит тесты. Mostly integration.". Not too many. Только давайте назовем это smoke tests — чисто проверить что ничего вроде бы не взрывается. Идея это не плохая, и если времени мало (и тесты особо не нужны) — так и надо делать.

Просто потому, что они будут расти и расти, и для того чтобы протестировать третью кнопочку справа, надо будет в начале нажимать на 3 кнопочки в меню, и не забыть залогиниться. Если же времени много, и тесты нужны — этой дорогой лучше не ходить, потому что писать хорошие integration тесты очень и очень ДОЛГО. В общем — вот вам комбинаторный взрыв на блюдечке.

Возможность начать тесты с некоторого уже готового состояния некоторой части приложения. Решение тут одно и простое (по определению) — юнит тесты. При этом не обязательно использовать ezyme — можно запускать и браузерные тесты, если душа просит. А точнее в уменьшение области тестирования с приложения или большого блока до чего-то маленького — юнита, чем бы он не был. Самое главное тут — иметь возможность протестировать что-то в изоляции.

Не любят по разным причинам: Изоляция — один из ключевых моментов в юнит тестировании, и то, за что юнит тесты не любят.

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

По первому пункту конечно же можно порекомендовать integration tests, они для того и придуманы — проверить как правильно собраны предварительно протестированные компоненты. Лично я тут проблем не вижу. Чем ваши "компоненты" отличаются от "не ваших" пакетов? Вы же доверяете npm пакетам, которые тестируют, конечно же, только сами себя, а не себя в составе вашего приложения.

И именно про этот пункт будет эта статья (а все до этого было так — введением) — про то как сделать "юнит" юнит тестируемым. Со вторым пунктом все немного сложнее.

Разделяй и Властвуй

Если взять за основу (что делают 99% разработчиков) статью Дэна Абрамова, то Presentation Component: Идея разделения Реакт компонент на "Container" и "Presentation" не нова, хорошо описана, и уже успела немного устареть.

  • Отвечают за внешний вид (Are concerned with how things look)
  • Могут содержать как другие presentation компоненты, так и контейнеры** (May contain both presentational and container components** inside, and usually have some DOM markup and styles of their own)
  • Поддерживают слоты (Often allow containment via this.props.children)
  • Не зависят от приложения (Have no dependencies on the rest of the app, such as Flux actions or stores)
  • Не зависят от данных (Don’t specify how the data is loaded or mutated)
  • Интерфейс основан на props (Receive data and callbacks exclusively via props)
  • Часто stateless (Rarely have their own state (when they do, it’s UI state rather than data))
  • Часто SFC (Are written as functional components unless they need state, lifecycle hooks, or performance optimizations)

Ну а Контейнеры — это вся логика, весь доступ к данным, и все приложение в принципе.

В идеальном мире — контейнеры это ствол, а presentation components — листья.

Ключевых моментов в определении Дэна два — это "Не зависят от приложения", что есть почти что академическое определение "юнита", и *"Могут содержать как другие presentation компоненты, так и контейнеры**"*, где особо интересны именно эти звездочки.

Я больше так не думаю. (вольный перевод) ** В ранних версиях своей статьи я(Дэн) говорил что presentational components должны содержать только другие presentational components. В общем не партесь и все будет окей. Тип компонента это детали и может меняться со временем.

Давайте вспомним, что происходит после этого:

  • В сторибуке все падает, потому что какой-то контейнер, в третьей кнопке слева лезет в стор которого нет. Особый привет graphql, react-router и другие react-intl.
  • Теряется возможность использовать mount в тестах, потому что он рендерит все от А до Я, и опять же где-то там в глубинах render tree кто-то что-то делает, и тесты падают.
  • Теряется возможность управлять стейтом приложения, так как (образно говоря) теряется возможность мокать селекторы/ресолверы(особенно с proxyquire), и требуется мокать весь стор целиком. А это крутовато для юнит тестов.

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

Вот простой пример из статьи "Почему я всегда использую shallow" В итоге приходится использовать shallow, который по дизайну избавляет от всех вредных(и неожиданных) сайд эффектов.

Представим что Tooltip отрендерит "?", при нажатии на который будет показан сам тип.

import Tooltip from 'react-cool-tooltip'; const MyComponent = () => </Tooltip>
}

Mount + нажать + проверить что видимо. Как это протестить? С shallow проблемы нет, так как мозгов и самого "чужого компонента" нет. Это integration test, а не юнит, да и вопрос как нажать на "чужой" для вас комопонент. А мозги тут есть, так как Tooltip — контейнер, в то время как MyComponent практически presentation.

jest.mock('react-cool-tooltip', {default: ({children}) => childlren});

"Компонент" резко стал сильно тупее, сильно короче, сильно конечнее. А вот если замокать react-cool-tooltip — то проблем с тестированием не будет.

Конечный компонент

  • компонент с хорошо известным размером, который может включать другие, заранее известные, конечные компоненты, или не содержащий их вообще.
  • не содержит в себе других контейнеров, так как они содержат неконтролируемый стейт и "увеличивают" размер, т.е. делают текущий компонент бесконечным.
  • во всем остальном — это обычный presentation component. По сути именно такой каким был описан в первой версии статьи Дэна.

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

Весь вопрос — как вынуть.

Решение 1 — DI

Дэн его тоже любит. Мое любимое — Dependency Injection. В двух словах — не нужно использовать Контейнеры внутри Presentation — их нужно туда инжектить. И вообще это не DI, а "слоты". А в тестах можно будет инжектить что-то другое.

// я тестируем через mount если слоты сделать пустыми
const PageChrome = ({children, aside}) => ( <section> <aside>{aside}</aside> {children} </section>
); // а я тестируем через shallow, просто проверь что в слоты переданы
// а может и через mount сработает? разок, так, чисто проверить wiring?
const PageChromeContainer = () => ( <PageChrome aside={<ASideContainer />}> <Page /> </PageChrome> );

Этот именно тот случай, когда "контейнеры это ствол, а presentation components — листья"

Решение 2 — Границы

Наверное сейчас %username% думает как его можно применить на текущей кодовой базе, и решение не придумывается... DI часто может быть крутоват.

В таких случаях вас спасут Границы.

const Boundary = ({children}) => ( process.env.NODE_ENV === 'test' ? null : children // // или jest.mock
);
const PageChrome = () => ( <section> <aside><Boundary><ASideContainer /></Boundary></aside> <Boundary><Page /></Boundary> </section>
);

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

Решение 3 — Tier

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

const checkTier = tier => tier === currentTier;
const withTier = tier => WrapperComponent => (props) => ( (process.env.NODE_ENV !== ‘test’ || checkTier(tier)) && <WrapperComponent{...props} />
);
const PageChrome = () => ( <section> <aside><ASideContainer /></aside> <Page /> </section>
);
const ASideContainer = withTier('UI')(...)
const Page = withTier('Page')(...)
const PageChromeContainer = withTier('UI')(PageChrome);

Суть не важна, главное что можно вытащить шестеренку, возможно не одну, но конечное колличество, как-то проведя границу между тем что нужно, и что не нужно (для разных тестов это граница разная). Под именем Tier/Layer тут могут быть разные вещи — feature, duck, module, или именно что layer/tier.

И ничего не мешает разметить эти границы как-то по другому.

Решение 4 — Separate Concerns

Если решение (по определению) лежит в разделении сущьностей — что будет если их взять и разделить?

А если нет — ничто не мешает прямо сейчас начать именовать Компоненты как-то более звучно. "Контейнеры", которые мы так не любим, обычно называются контейнерами. Или они имеют в имени некий паттерн — Connect(WrappedComonent), или GraphQL/Query.

Что если прямо в рантайме провести границу между сущьностями на основе имени?

const PageChrome = () => ( <section> <aside><ASideContainer /></aside> <Page /> </section>
); // remove all components matching react-redux pattern
reactRemock.mock(/Connect\(\w\)/)
// all any other container
reactRemock.mock(/Container/)

Плюс одна строчка в тестах, и react-remock уберет все контейнеры, которые могут помешать тестам.

В принципе такой подход можно использовать и для тестирования самих контейнеров — просто понадобиться убирать все кроме первого контейнера.

import {createElement, remock} from 'react-remock'; // изначально "можно"
const ContainerCondition = React.createContext(true); reactRemock.mock(/Connect\(\w\)/, (type, props, children) => ( <ContainerCondition.Consumer> { opened => ( opened ? ( // "закрываем" и рендерим реальный компонент <ContainerCondition.Provider value={false}> {createElement(type, props, ...children)} <ContainerCondition.Provider> ) // "закрыто" : null )} </ContainerCondition.Consumer>
)

Опять же — пара строчек и шестеренка вынута.

Итого

Кто-то махает рукой на юнит тесты и переносит все в Cypress (гулять так гулять!). За последний год тестирование React компонент усложнилось, особенно для mount — требуется овернуть все 10 Провайдеров, Контекстов, и все сложнее и сложее протестировать нужный компонент в нужном стейте — слишком много веревочек, за которые нужно дергать.
Кто-то плюет и уходит в мир shallow.

Все примеры выше — по сути использование этих algebraic effects и моков. Кто-то другой тыкает пальцем в реакт, говорит что это algebraic effects и можно делать что захочешь. Для меня и DI это моки.

S.: Этот пост был написан как ответ на комент в React/RFC про то что команда Реакта все сломало, и все полимеры туда же
P. P. S.: Этот пост вообще-то очень вольный перевод другого
PPPS: А вообще для реальной изоляции посмотрите на rewiremock P.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Из песочницы] Прибыльность сайтов и сервисов

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

Прошлое и будущее Java в интервью с Саймоном Риттером из Azul

Представляем вам интервью с Саймоном Риттером — человеком, который работал над Java с самого начала и продолжает делать это в роли заместителя технического директора Azul — компании, работающей над виртуальной машиной Zing JVM и одним из лучших сборщиков мусора, C4 ...