Как я взломал архитектуру компонентов пользовательского интерфейса и построил надежные и многоразовые тесты

Как back-end разработчик, погружающийся в мир разработки Vue, я быстро понял, что модульное тестирование будет иметь решающее значение для успеха моего продукта. Создавалось впечатление, что разработка пользовательского интерфейса была похожа на игру в игру «Ударь крота», где исправление одной вещи приводит к отмене другой, исправленной ранее.

Сначала я думал, что тестирование компонентов означает, что мне нужно сравнивать вывод DOM с комбинацией свойств, слотов, триггеров и источников данных. Но написание тестов для проверки HTML-кода компонента казалось, что это приведет к хрупким, громоздким тестам, непрактичным для моей быстро развивающейся базы кода. Вместо этого я сместил свое внимание с вывода HTML в целом и обратился к более целевым результатам на основе входных данных, которые повлияли на поведение моих компонентов.

Моя установка

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

Супер простой пример

Начнем с компонента Vue, который отображает сообщение пользователю:

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

Но что, если шаблон изменится на что-то вроде следующего?

<template>
  <div style="background-color: green">
    <p>{{ message }}</p>
  </div>
</template>

Несмотря на то, что шаблон изменился, основная концепция теста осталась неизменной. Назначение поля message - убедиться, что оно отображается в компоненте, и это назначение практически не изменилось между этой версией и ее предшественником.

А как насчет background-color: green? Разве это не должно быть проверено с помощью компонентного теста? В данном конкретном случае нет, потому что это значение статично для самого шаблона. Компонент формально не предписывает способ изменения этого значения и, следовательно, не имеет решающего значения для достоверности самого компонента. Если отбросить постороннюю информацию, останется следующее.

И это описывает тот же компонент, который был построен изначально.

Компоненты с братьями и сестрами

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

Теперь тест должен измениться, поскольку внутренний текст корневого компонента теперь выглядит примерно как 'This is my demo!'+ {{ some whitespace }} + message. Как я упоминал ранее, статические данные не имеют отношения к поведению опоры. Наивный способ переписать этот тест был бы примерно таким:

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

И итоговый тест будет выглядеть так:

Таким образом, независимо от того, как будет развиваться шаблон, ссылка messageField всегда будет связана со значением свойства message.

Стилистические изменения компонента

Что, если наш компонент содержит свойство, изменяющее цвет фона?

Правдивость атрибута success определяет результат атрибута background-color. Следовательно, необходимы дополнительные тесты для проверки этого конкретного поведения.

Обратите внимание, что хотя компонент имеет свойство message, он не описывается и не упоминается ни в одном из этих тестов. Почему? Потому что message не влияет на то, будет ли изменен цвет компонента. Другой способ подумать об этом - вместо компонента Vue, что, если бы этот объект был описан как класс:

Если вы проверяете полученное значение успеха, вы должны установить и вызвать .success. Установка и получение значения .message не влияет на результат поля .success и должен иметь свой собственный отдельный набор проверок.

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

Можно написать тест для проверки стиля элемента div, а затем проверить стиль элемента p. Но что, если будут добавлены или изменены другие стилистические особенности? Это становится немного хлопотно и, в свою очередь, делает тест более хрупким. В конце концов, тестирование компонентов на самом деле не столько в том, как компонент выглядит, сколько в том, как он функционирует. Таким образом, функционально эта реализация эквивалентна следующему.

И тест будет такой:

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

В этом случае тест по-прежнему работает, потому что класс CSS success-me является основным результатом того, что происходит, когда задано свойство success.

Компоненты внутри компонентов

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

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

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

Разделение и разделение компонентов

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

  • Одним предложением опишите, что делает компонент. Если вы не можете описать цель, не используя слова и, тогда каждое из этих предложений представляет потенциальные подкомпоненты.
The purpose of this component is to display a toolbar (1) AND left navigation (2) AND main content (3).
  • Следующий шаг - описать, как компонент выполняет свою задачу. Опять же, если вы не можете описать, как это сделать, не используя слова и, тогда каждое из этих предложений представляет собой потенциальный новый подкомпонент.
This component displays the weekly calendar.  It displays the calendar by showing the days of the week (1) AND the hours of the day (2) AND scheduled events (3) AND holidays (4)

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

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

Спасибо за прочтение!