Компоненты и директивы AngularJS (Angular 1.5+) великолепны. Компонент используется для инкапсуляции элемента пользовательского интерфейса, а также его взаимодействия с пользователем. Компонент является понятным, многоразовым и может быть протестирован как единое целое без использования какой-либо реализации Selenium (например, Protractor).

При поиске способов тестирования компонентов вы, вероятно, встретите два основных широко используемых подхода. Первый - протестировать контроллер без представления (например, в Документации по угловым компонентам). Второй использует службу $ compile, предоставляемую AngularJS, для визуализации компонента и доступа к нему через селекторы и триггеры DOM (щелчок, изменение и т. Д.).

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

Позвольте мне продемонстрировать этот новый подход на примере.
Предположим, что простое дерево компонентов представляет собой список имен:

Шаблон главного компонента, вероятно, будет выглядеть примерно так:

Как вы могли заметить, он использует два подкомпонента - список имен и ввод имени, данные передаются в них как атрибуты (имена), а данные возвращаются как события (onNameAdded ).

Давайте посмотрим на список имен и реализацию компонента средства форматирования имен:

Тестирование этих компонентов старым (и неполным…) способом

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

Как только контроллер извлечен, мы можем провести его модульное тестирование:

Нет необходимости продолжать этот тест, чтобы выявить несколько проблем:

  1. Мы изменили код, чтобы иметь возможность тестировать контроллер, модульное тестирование чего-либо не должно требовать корректировок в производственном коде.
    В качестве альтернативы мы могли бы использовать $ componentController, который избавит от регистрации контроллера на модуле, но остальное все еще требуется, и все вопросы по-прежнему актуальны.
  2. Нам пришлось раскрыть большую часть внутреннего поведения компонента, метод getFormattedName на самом деле не является общедоступным API, почему тест должен терпеть неудачу, если я решу его переименовать?
  3. Нет никакой гарантии, что представление действительно будет использовать метод, который мы только что протестировали, если по ошибке я не вызову его должным образом, вызвав {{$ ctrl.getName ()}}, тест все равно пройдет, Я могу полностью удалить элемент, но тест все еще остается зеленым.

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

AngularJS предоставляет отличные утилиты и службы, которые позволяют тестировать пользовательский интерфейс на уровне UT (AKA View Unit Test), следуя лучшим практикам, мы можем переписать тесты следующим образом:

И список имен (неполный список тестов):

Этих двух компонентов достаточно, чтобы заметить список проблем при их тестировании:

  1. Шаблон повторяется в каждом тесте - я мог бы немного уменьшить его, используя обратный вызов beforeEach, но это означает, что я могу только визуализировать компонент таким же образом снова в каждом тесте, не имея возможности тестировать он с разными атрибутами (например, для проверки поведения, когда список имен пуст)
  2. Если компонент является частью библиотеки, любой, кто использует эту библиотеку, должен будет имитировать логику, чтобы иметь возможность тестировать компоненты, которые ее используют.
  3. Тесты связаны с используемым фреймворком, есть сочетание значений, селекторов, вызовов $ digest и т. Д. Необходимо иметь глубокое понимание фреймворка, чтобы понять, какова цель теста / приложения. является.

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

Я уверен, вы знаете, что взаимодействие между вашей операционной системой и любым аппаратным компонентом на самом деле не так. Это было решено созданием драйвера устройства:

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

Даже наше взаимодействие с веб-страницей осуществляется с помощью какого-то драйвера (также известного как ваш браузер), который сопоставляет щелчки, нажатия клавиш и движения мыши событиям и триггерам. Он также преобразует текст (HTML и CSS) в графический интерфейс.
Разве мы не можем использовать тот же подход для тестирования компонента пользовательского интерфейса?

Да мы можем!

Большинство статей, относящихся к тестированию компонентов (если не все), описывают использование заглушек и драйверов. По какой-то причине при тестировании наших компонентов AngularJS (или React / Backbone / you-name-it-js) мы склонны использовать заглушки (например, jasmine / mocha spies) для изоляции компонента от служб и других компонентов, но не драйвера. чтобы изолировать компонент от того, как браузер (и пользователь) взаимодействует с нашим компонентом.
Драйвер для компонента пользовательского интерфейса предоставит набор читаемых пользователем методов, изолируя, как компонент написан и визуализирован, от того, что мы хотим для тестирования.
Итак, давайте определим драйвер для взаимодействия с нашими компонентами и будем использовать их в тестах, которые мы создали выше:

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

Отделение логики DOM от тестовых селекторов

Драйвер для списка имен должен знать имя средства форматирования имени (он запрашивает имя элемента). Это может быть решено путем определения атрибутов data- * в представлении, чтобы определить селекторы, которые предназначены для доступа к конкретному элементу без необходимости знать, какие классы CSS он использует.
Я обычно использую для этой цели атрибут data-hook.
Чтобы использовать его в нашем примере, мы внесем небольшие изменения в представление списка имен:

И водитель:

Но это все еще не идеально…

  1. Существует связь между тестированием списка имен и средством форматирования имен, изменение логики средства форматирования имен приведет к ошибке второго теста списка имен.
  2. Повторного использования между тестами нет. Чтобы протестировать компонент списка имен, необходимо получить доступ к элементам, определенным во вложенных компонентах (список компонентов средства форматирования имен), что требует переопределения тех же селекторов, которые определены в NameListComponentDriver, чтобы получить доступ к форматированному имени, изменяя представление форматирования имен потребует настройки в двух драйверах.
  3. Кажется, есть какой-то код, который повторяется между драйверами - подрядчики идентичны, а метод рендеринга почти идентичен

Разве не было бы замечательно, если бы мы могли решить все перечисленные выше проблемы?
Иметь какую-то тестовую утилиту, которая устранит все эти хлопоты?
Маленькая утилита, которая позволит нам иметь драйверы, которые имеет дело только с реальной логикой компонента без шаблонов?
Что, если бы я сказал вам, что такая утилита уже существует?

Представляем TurnerJS !!!

Небольшая утилита, которую мы создали здесь, на Wix.com. Он позволяет создать драйвер (на TypeScript и / или ECMAScript 6 или даже на обычном ECMAScript5), который упрощает стандартные проблемы и позволяет создавать простые средства доступа и сеттеры для взаимодействия с вашим компонентом в тестах.
Драйверы могут повторно использоваться другими драйверов, поскольку TurnerJS поддерживает вложение драйверов - драйвер может создать вспомогательный драйвер или даже список дочерних драйверов (например, при тестировании ng-repeat) и использовать их для взаимодействия с вспомогательными компонентами.

С TurnerJS драйвер форматирования имен короче и чище:

Здесь представлены 3 API, предоставляемых библиотекой TurnerJS:

  1. Драйвер расширяет базовый драйвер компонента, включенный в TurnerJS (TurnerComponentDriver)
  2. renderFromTemplate - этот метод заботится о рендеринге шаблона, создании новой области и применении изменений (запуск дайджеста). Он принимает два параметра: шаблон для рендеринга и объект, который описывает, какие параметры определены в шаблоне. Например, поскольку средство форматирования имени ожидает атрибут имени, мы должны передать его этому объекту, чтобы использовать его в шаблоне.
  3. findByDataHook - встроенный селектор для запроса элемента с помощью атрибута привязки данных (или элементов с помощью findAllByDataHook)

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

Вы можете заметить, что мы использовали другой метод TurnerJS - defineChildren, который позволяет определять набор вложенных драйверов компонентов таким образом, чтобы каждый из них взаимодействовал с одним экземпляром вложенного компонента (defineChild может быть используется, когда существует ровно один экземпляр вложенного компонента).

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

Что еще более удивительно, когда список имен изменяется, нет необходимости снова определять драйверы имен, исходный массив драйверов имен был изменен, чтобы отразить обновленное значение - TurnerJS обновит сам массив для вас каждый раз при изменении dom (ура!)
Это позволяет создавать тесты, которые проверяют динамическое поведение списка:

Драйверы разделяют ту же иерархию, что и приложение, поэтому каждый драйвер должен иметь дело только с логикой компонента, с которым он взаимодействует, любой подкомпонент взаимодействует с помощью своего драйвера. Драйвер для основного приложения даже не знает о NameFormatterComponentDriver, но он может быть заинтересован в получении всех отформатированных имен, чтобы проверить, что новое добавленное имя находится в списке, поэтому ему нужно использовать только getFormattedNames в своем дочернем драйвере, и TurnerJS автоматически создаст связь между NameListComponentDriver и его вложенным драйвером (или любым другим определенным дочерним элементом), чтобы иметь возможность получить отформатированное имя каждого имени в список.

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

Теперь давайте воспользуемся ими, чтобы создать пару тестов для имени приложения.

Выглядит довольно ясно и просто, правда? Но просто посмотрите, что на самом деле проверяет тест. Это проверка того, что интеграция между приложением имени и списком имен работает, это проверка того, что приложение имени может отображать ввод и использовать его для обновления списка.

Я не буду включать все тесты в эту статью, но вы можете увидеть их в примере приложения, включенном в репозиторий на github с открытым исходным кодом (он написан на TypeScript, но может быть легко прочитан как ES6 Javascript).
Там вы также найдете пример тестов на основе ES5 (без классов).
Кстати, я использовал TurnerJS для тестирования старых директив AngularJS 1.3, поэтому он предназначен не только для компонентов, написанных на AngularJS 1.5+.

Итак, что мы получили?

  1. TurnerJS поддерживает иерархии, поэтому драйверы можно повторно использовать в родительских компонентах.
  2. Использование драйверов упрощает моделирование модульного тестирования на основе пользовательского интерфейса, больше нет необходимости вводить сервисы $ Complie / $ rootScope.
  3. Тесты намного удобочитаемы, понятно, для чего предназначен каждый тест. Если кого-то интересует логика взаимодействия с пользовательским интерфейсом, он может обратиться к самому драйверу, поэтому есть разделение и на заботу, и на контроль.

Кроме того, представьте, что каждая сторонняя библиотека будет предоставлять API для фазы тестирования, чтобы взаимодействовать с создаваемым компонентом.
Например, представьте, что средство выбора даты Angular Bootstrap включало бы DatePickerDriver с помощью метода selectDate (date), который можно использовать при тестировании компонентов с помощью datepicker, что позволит сэкономить массу сложных имитаций и заглушек.

Такой подход позволяет создавать компоненты пользовательского интерфейса меньшего размера, тестировать каждый с повторно используемым драйвером и строить (и тестировать) все более и более крупные компоненты, каждый из которых имеет определенную роль. А позже, используя их драйверы, создайте несколько тестов, которые проверяют только интеграцию между компонентами, без необходимости иметь дело со всем приложением для каждого сценария (например, нам не нужно тестировать пустую проверку ввода, поскольку она будет проверена в name input tests).
Я создал полные страницы, построенные из нескольких компонентов, даже без необходимости открывать браузер (и использовать браузер позже только для завершения стилей и повышения резкости анимации).

Известные ограничения

TurnerJS используется для модульного тестирования представлений и, как таковой, имеет те же ограничения, что и другие предложения / фреймворки / руководства по тестированию компонентов / директив:

  1. Определения CSS - вы можете загружать файлы CSS при тестировании, но возможно, что некоторые из определений не будут применяться, поскольку они основаны на родительских элементах, которые не включены в шаблон компонента. По этой причине вы не можете проверить, что ng-show / ng-hide действительно сделает элементы невидимыми (ng-if отлично работает)
  2. Перекрывающиеся элементы - относятся к предыдущему элементу, но важно отметить, что если элемент A скрывает элемент B, элемент B по-прежнему доступен для нажатия при доступе с помощью селекторов, поэтому фреймворки на основе Selenium (такие как Protractor) по-прежнему необходимо проверить, действительно ли элемент доступен для кликов
  3. Навигация - модуль представления предназначен для тестирования одного компонента за раз, поэтому нет возможности проверить навигацию между одним компонентом страницы и другим, можно проверить, что маршрут изменился, но это не так. полный тест.

А теперь, когда вы все в восторге, что дальше?

Я могу продолжать и говорить о Тернере (как о библиотеке JS, так и о стадионе), но я надеюсь, что мне удалось разжечь ваше любопытство, и вы попробуете. Просто зайдите на сайт TurnerJS и попробуйте.
Я могу честно сказать, что он полностью изменил способ тестирования пользовательского интерфейса некоторыми командами Wix.com и даже то, как написан производственный код ( более компонентно-ориентированный), поэтому я могу только надеяться, что он сделает то же самое для вас.