Шаблон для полноценного модульного тестирования в Salesforce Lightning

Недавно я попробовал Salesforce's Lightning Testing Service (LTS) для написания модульных тестов для наших компонентов Lightning. Я обнаружил, что примерные тесты, которые Salesforce предоставляет с LTS, действуют как интеграционные тесты, проверяя функциональность компонента и любых вложенных компонентов в целом, включая взаимодействие с фреймворком Aura. Недавно переработав наш код Apex для обеспечения истинной модульной тестируемости, мы хотели провести модульное тестирование нашей логики JavaScript, возможно, используя LTS, чтобы предоставить способ имитировать зависимости и фреймворк Aura. LTS этого не делает, но я спроектировал и внедрил решение, которое позволяет нам действительно использовать вспомогательные методы для модульного тестирования.

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

Пример использования

Если пользователь нажимает кнопку удаления в нашем компоненте, мы показываем модальное окно с просьбой подтвердить удаление. При подтверждении вызывается deleteItem() в контроллере. Мы хотим протестировать поведение в deleteItem().

Как видите, этот метод довольно многословен, но его можно резюмировать следующим образом: мы показываем счетчик, затем делаем запрос к Apex на удаление элемента; в случае успеха мы переходим на другую страницу; а в случае неудачи мы скрываем приглашение, скрываем счетчик и показываем всплывающее сообщение об ошибке.

Рефакторинг

Шаг 1. Переименуйте методы контроллера как события

В этом новом шаблоне каждый метод контроллера представляет событие, запускаемое платформой (onInit) или взаимодействием с пользователем (onDeleteConfirmed). Думайте о контроллере как о точке входа в вашу логику JS. Лучше называть методы вашего контроллера независимо от элементов пользовательского интерфейса, с которыми они связаны, т. Е. Выбирать onSaveRequested, а не onSaveClick. Таким образом, вы можете обновить реализацию пользовательского интерфейса или полностью переместить ее в другой компонент, не затрагивая логику JS.

Шаг 2. Переход на помощник

Это довольно просто: просто переместите всю логику из метода контроллера во вспомогательный метод. Если ваш вспомогательный метод выполняет несколько задач, назовите его в честь метода контроллера, но с префиксом handle вместо on. Если ваш вспомогательный метод выполняет одну единицу логики, то есть вызывает Apex для сохранения элемента, тогда назовите его после выполненного действия, начиная с глагола, например saveItem.

Шаг 3. Обещание

Метод, который мы собираемся протестировать, handleDeleteConfirmed, теперь гораздо удобнее читать из-за сильной компонуемости Promises. Возврат обещания из вспомогательного метода является требованием для тестирования. Поскольку мы имеем дело с асинхронной работой, состояние решения или отклонения обещания сообщает инфраструктуре тестирования, когда начинать утверждения. Дополнительным преимуществом этой абстракции является то, что реализация может измениться, например Выноска Apex против Lightning Data Service, но логика в handleDeleteConfirmed не обязательна.

Шаг 4. Абстрагируйтесь от деталей реализации

Веб-разработка постоянно развивается, как и Lightning. В прошлом вы могли использовать настраиваемый компонент для отображения модального окна, но теперь вы можете использовать вместо него встроенный lightning:overlayLibrary. С реализациями ваших компонентов пользовательского интерфейса, абстрагированными от вашей бизнес-логики, вы можете поменять местами эти реализации и оставить любую существующую логику нетронутой.

Шаг 5. Введите значения, а не get их

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

someMethod: function(component) {
    component.get('v.item'); // {sortOrder: 2, ...}
    this.doStuff(component);
    this.fireEvent(component);
    this.evaluateOtherItems(component);
    component.get('v.item'); // null
    this.evaluateItem(component);
},
evaluateItem: function(component) {
    const item = component.get('v.item');
    if (item.sortOrder !== 0) { // ERROR!
        // ... other logic
    }
}

evaluateItem() не заботится о том, что сделано в someMethod(), его единственная задача - применить логику к элементу. Также не очевидно, какой метод, вызываемый в someMethod(), изменяет v.item.

Внедрение результатов component.get() в метод похоже на работу со снимком состояния компонента. Вы уменьшите двусмысленность, уменьшите вероятность ошибок и останетесь с хорошо поддающимся тестированию кодом.

Я также рекомендую добавить в ваш метод отдельные параметры из event.getParams() по тем же причинам. Кроме того, это ослабляет связь между вашей бизнес-логикой и тем фактом, что она была инициирована событием.

Вы уменьшите двусмысленность, уменьшите вероятность ошибок и останетесь с хорошо поддающимся тестированию кодом.

Включение модульного тестирования

Добавьте этот HelperProvider компонент в свою aura папку и расширьте его из компонента, который вы хотите протестировать:

<c:MyComponent … extends=”c:HelperProvider”>

Проверка $T, которая определена LTS, требует, чтобы помощник был предоставлен только во время выполнения тестов.

Вы также можете использовать isTest(), чтобы предотвратить запуск любого onInit кода во время создания компонента в тесте:

({
    onInit: function(component, event, helper) {
        if (!helper.isTest()) {
            helper.handleOnInit(…);
        }
    }
    //…
})

Модульное тестирование

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

… Мы показываем счетчик, затем отправляем запрос в Apex на удаление элемента; в случае успеха мы переходим на другую страницу; а в случае неудачи мы скрываем приглашение, скрываем счетчик и показываем всплывающее сообщение об ошибке.

Вот последняя часть этой реализации - модульные тесты.

Давайте разберемся:

показываем блесну,

Проверено it(‘shows a spinner’, …) путем проверки вызова showSpinner().

затем отправьте запрос в Apex на удаление элемента;

Проверено it(‘attempts to delete the item’, …) путем проверки deleteItem() вызова i.

при успехе,

Инкапсулировано describe(‘on deletion success’, …). Поскольку мы имитируем deleteItem(), чтобы вернуть обработанное обещание в beforeAll(), нет необходимости настраивать какие-либо другие имитаторы.

переходим на другую страницу;

Проверено it(‘redirects to another page’, …) путем проверки вызова navigateToAnotherPage().

а в случае неудачи

Инкапсулировано describe(‘on deletion failure’, …). Насмешка, выполненная в beforeEach(), гарантирует, что deleteItem() возвращает отклоняющее обещание.

мы скрываем подсказку,

Проверено it(‘hides deletion prompt’, …) путем проверки вызова hideDeletePrompt().

спрятать спиннер,

Проверено it(‘hides spinner’, …) путем проверки вызова hideSpinner().

и показать тост с ошибкой.

Проверено it(‘shows an error toast’, …) путем проверки вызова showErrorToast().

Заключение (TL; DR)

  • Lightning Testing Service упрощает интеграционные тесты.
  • Модульный тестируемый код - это код, который не тесно связан с другими реализациями или фреймворком, и чьи зависимости либо передаются, либо могут быть имитированы.
  • Шаблон, описанный в этом посте, является хорошим руководством для написания тестируемого кода.
  • HelperProvider обеспечивает доступ и возможность модульного тестирования вспомогательных методов компонента.

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