Статьи

Гексагональная архитектура в JavaScript

Цель: модульное тестирование кода JavaScript в отрыве от фреймворков, на стороне сервера и DOM.

Для достижения этой цели разрабатываемый код должен содержать некоторую логику, такую ​​как вычисления, манипуляции со строками, конечный автомат и так далее. Если это всего лишь обратный вызов, окрашивающий кнопку в синий оттенок при нажатии, то тестирование ее изолированно не добавляет много к изображению. Вместо этого тестирование конечного автомата, описывающего представления одностраничного приложения, является более интересным и имеет более высокий ROI.

Архитектура: порт и адаптеры

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

Очень популярной схемой, которая позволяет включать модульное тестирование, является гексагональная архитектура (которая не обязательно имеет шесть сторон).

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

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

Идея архитектуры состоит в том, чтобы удовлетворить каждый порт адаптером, содержащим код, который «проблематичен» для тестирования; в этом случае трудность возникает из-за ссылок на фреймворки, Ajax и DOM. Ядро остается свободным от внешних зависимостей и может быть детерминировано протестировано в одном процессе.

Ведущая сторона

Чтобы иметь возможность проверить ядро, нам нужно эмулировать адаптеры для стороны вождения. Кто вызывает код JavaScript внутри вашего приложения?

Эти порты могут естественно соответствовать тому, что требует фреймворк или плагин:

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

Например, jQuery (‘# my_form’). Submit () позволяет вам указать обратный вызов для вызова, когда пользователь отправляет форму. Вместо того, чтобы указывать его как анонимную функцию, его обычно легко параметризировать и тестировать независимо:

var form = $('#my_form');
form.submit(Application.formSubmit($('div#result')));

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

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

Между прочим, не только управляющие порты должны определять интерфейс, который легко тестировать: $ и подобные библиотеки — это управляемые порты, которые тоже нуждаются в двойном тестировании.

Ведомая сторона

Управляемые порты определяются как объекты и функции, которые вы хотите заменить тестовыми двойниками, чтобы проверить свою логику изолированно; например, не делать реальный вызов Ajax, а подставлять запрос функцией, которая возвращает предопределенное значение ( здесь описан конкретный случай Ajax ).

В общем, есть два варианта:

  • представьте Test Double, который напрямую реализует API, вызываемый вашим кодом (jQuery.get, jQuery.css или XMLHttpRequest.)
  • Оберните оригинальную библиотеку и определите свой собственный интерфейс, который вы можете заменить.

Принцип «Только типы насмешек, которыми вы владеете» указывает на второй выбор, иначе мы не смогли бы использовать насмешку как инструмент дизайна. Интерфейс будет исправлен, и вы будете застревать с вызовом тех универсальных каркасных методов, которые ожидают в качестве аргументов объект JSON из 10 полей.

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

var old$ = $;
$ = function(selector) {
    // mocked behavior
};
// test of an object calling $()
$ = old$;

в то время как если есть нарушение закона Деметры, например, $ (‘# selector’). css (‘..’). html (‘…’). click (…), то напрямую издеваться будет больно .

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

Вот как выглядит подход обтекания и макета:

// real implementation
var changeAppearance = function(element, css, html) {
    element.css(css).html(html);
};
// production code
var factory = function(element, appearance) {
    return function() {
        // logic to test
        appearance(element, css, html);
    };
};
// test
var mockAppearance = function(element, css, html) {
    assertEquals(css, ...);
    assertEquals(html, ...);
};
var objectUnderTest = factory(dummyElement, mockAppearance);
objectUnderTest();

Выводы

Чем больше логики у вас внутри JavaScript, тем больше вы должны заботиться о его тестировании независимо от всего, что происходит на стороне клиента. Эволюция в направлении моделирования портов, Ajax и DOM является естественной; Следующим шагом является внедрение адаптеров для дополнительной изоляции ядра от внешних проблем, предоставляя ему приятный API вместо джунглей вызовов платформы.