В Koan есть большой React SPA, который интенсивно использует селекторы Redux. Это упрощает получение данных из нашего хранилища Redux в точном производном формате, который нам нужен, но для их работы требуется заполненное хранилище. Это нормально для производственной среды, но при написании модульных тестов компонентов обычно не требуется столько преамбулы только для проверки функциональности нашего компонента.

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

Эта проблема

Многие из наших компонентов выглядят примерно так:

Здесь мы постарались сделать наш компонент функцией без сохранения состояния, поэтому мы знаем, что эту часть будет легко протестировать изолированно. А как насчет тестирования mapStateToProps? FooComponent может быть очень простым, но теперь мы просто переложили сложность на другую функцию. И хотя это выглядит достаточно просто, есть проблема: этот вызов fooSelector.

Учитывая только входные данные (nextReduxState и nextOwnProps), мы не можем детерминированно сказать, каким будет выход функции, то есть это не чистая функция.

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

Компонент более высокого порядка внедрения зависимостей

Решение, к которому мы пришли, использует всеми любимую абстракцию React, компонент высшего порядка (HOC) и его лучший друг, библиотеку утилит HOC Recompose. Сначала мы создадим HOC, который просто принимает компонент и возвращает тот же компонент, но с fooSelector опорой:

Здесь мы используем withProps, чтобы внедрить модуль fooSelector в качестве опоры для компонента, при этом оставив остальные его опоры нетронутыми.

Теперь мы перепишем наш исходный компонент с новым HOC:

Здесь мы использовали compose, чтобы связать наши HOC таким образом, чтобы mapStateToProps мог использовать fooSelector без необходимости касаться чего-либо, кроме его собственных аргументов! Эта идея заимствована из фреймворков внедрения зависимостей (DI) в таких языках, как Java. Теперь наш модульный тест может выглядеть примерно так:

Почему бы не использовать опору для рендеринга?

Размышляя об этом, мы думали, действительно ли HOC был правильным подходом. В конце концов, рендеринг реквизита, похоже, набирает обороты в сообществе React, и его использование позволило бы нам избежать использования Recompose, а также необходимости вручную переносить несколько слоев HOC. Так почему мы все же выбрали маршрут HOC? Давайте посмотрим, как может выглядеть версия withFooSelector для рендеринга:

А вот как FooComponent будет выглядеть в сценарии рендеринга:

Итак, что здесь изменилось? Что ж, вместо HOC у нас теперь есть эти WithFooSelector и WrappedFoo компоненты, которые, кажется, делают то, что делали withFooSelector, только теперь у нас есть и компонент render prop, и его экземпляр, с которым нужно иметь дело. При подходе HOC нам нужен был только один компонент-оболочка, и все готово. Это довольно незначительный компромисс, поэтому кажется, что реквизиты для рендеринга могли бы быть здесь прекрасным выбором, но мы решили избежать необходимости создавать новый Wrapped* компонент для каждого случая, когда нам нужно было внедрить fooSelector. И хотя эту дополнительную упаковку можно было бы абстрагировать, чтобы уменьшить шаблонность, мы, вероятно, в конечном итоге окажемся в более или менее том же месте, что и с HOC.

HOC как внедрение зависимостей

Используя этот подход, мы показали, что без реальной инфраструктуры DI мы можем получить многие из преимуществ DI, включая возможность замены зависимостей и возможность модульного тестирования. Если бы у нас был второй fooSelector, который мы хотели бы использовать с нашим компонентом в другой части приложения, мы могли бы легко сделать это без изменения каких-либо внутренних компонентов нашего компонента и, конечно же, без добавления какой-либо сложной условной логики! Мы также смогли превратить большую часть нашей основной логики приложения в чистые функции, что упростило написание тестов.

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

Мы также подвергаемся риску того, что наша имитируемая версия fooSelector в нашем модульном тесте будет отклоняться от реальной реализации в modules/selectors/fooSelector. Это распространенный риск насмешек в модульных тестах, и он не может быть полностью решен даже с помощью ручных имитаторов Jest. Один из подходов, которые мы можем использовать для снижения этого риска, - это использовать фейки, а не имитировать: это потребует от нас поддержки единственной поддельной реализации fooSelector в нашем репо и внедрения этого в компоненты в наших тестах. . Хотя мы по-прежнему подвержены риску ложного отклонения от реальной реализации, мы бы объединили этот риск в один модуль в нашем репо.

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