Визуальные тесты с Galen Framework. Улучшаем читабильность кода

Два года прошло с момента написания первой статьи о Galen Framework. На тот момент все, что из себя представлял Galen, это лишь простенький набор проверок для расположения элементов страницы относительно других элементов. Тогда еще в нем не было ни возможности проверить скриншот по-пиксельно, ни расширить язык Galen Specs, который, собственно, и является основой фреймворка. Также тесты могли запускаться только с использованием одного формата тест-сьютов, что очень ограничивало возможности Galen тестов. С тех пор, благодаря поддержке сообщества, многое изменилось в Galen. Сегодня, это уже полноценный инструмент для визуального тестирования, который может не только проверять скриншоты по-пиксельно и накладывать фильтры на тестируемые изображения, но также предоставляет богатый набор фич, позволяющих расширять возможности языка Galen Specs. В этой статье я бы хотел продемонстрировать новые возмножности языка Galen Specs, а также показать, как улучшить читаемость визуальных тестов в Galen Framework на примере этой страницы.

5ee3fc43b6c8465683d1449663bad363.png

Читабельность — одно из важных свойств любого тест-кода. Один мой сотрудник даже утверждал, что читабельность тестов важнее читабельности основного кода, т.к. именно тест будет входной точкой при попытке разобраться, как работает какой-либо функционал в приложении. Мне эта идея понравилась, и я решил применить ее для визуальных тестов в Galen Framework. Моей целью было написать тест, прочтя который, станет понятно, как должен выглядеть сайт в разных размерах браузера. Давайте взглянем на самые распространенные случаи при тестировании верстки адаптивного сайта и попробуем разобраться, как же улучшить наши тесты.
С момента выхода первой версии Galen Framework все мои попытки написать понятный визуальный тест проваливались. Поначалу тестовый код был понятен, но, как только количество элементов на странице переваливало за 10, становилось уже трудно понимать и поддерживать эти массивные наборы инструкций.
Один из самых распространенных примеров — горизонтально расположенные повторяющиеся элементы (напр. меню)

630bd5997b304b7894568a95bd1419ac.png

Наш тестовый сайт в десктопном разрешении выглядит вот так:

d5a7a5e38ae14b2b968c8e5c66789e5b.png

Чтобы протестировать элементы горизонтального меню в Galen, используя стандартные инструкции, приходится писать подобный код:

@objects
    menu                     #menu ul
        item-*                  li a

= Menu =
    @forEach [menu.item-*] as menuItem, next as nextItem
        ${menuItem}:
            left-of ${nextItem} 0 to 5px
            aligned horizontally all ${nextItem}

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

Запустим же этот тест и посмотрим на отчеты:

galen check homepage.gspec --url http://galenframework.github.io/galen-extras/website/index.html --size 1024x768 --htmlreport reports

В итоге получим такой отчет:

aef6360fbd974810919aa8557e60d365.png

Не очень информативно. Когда смотришь подобные отчеты или пытаешься вникнуть в тест-код, всегда возникают вопросы: зачем была выбрана та или иная проверка? что именно тут хотели протестировать? и т.д.

Так как же можно улучшить этот код? На помощь приходят так называемые Custom Rules. Если вкратце, то это нечто похожее на функцию, только параметры распарсиваются из обычного предложения, написанного пользователем. Пока оставим параметризацию на потом и попробуем сначала реализовать простейшее правило. Например, для вышеуказанного случая мы можем создать выражение «menu items are aligned horizontally next to each other». Чуть позже мы добавим в это выражение параметры. А чтобы оно не мозолило нам глаза в основном тест файле, перенесем его в файл my-rules.gspec:

@rule menu items are aligned horizontally next to each other
    @forEach [menu.item-*] as menuItem, next as nextItem
        ${menuItem}:
            left-of ${nextItem} 0 to 5px
            aligned horizontally all ${nextItem}

Теперь вместо этого кода в основном файле мы сможем вызвать эту функцию вот таким образом

@import my-rules.gspec

@objects
    menu                     #menu ul
        item-*                  li a

= Menu =
    | menu items are aligned horizontally next to each other

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

e70be1de26364f998c693ca4822c18a4.png

Теперь мы можем улучшить наше выражение, добавив в него параметры. Например, если мы посмотрим на страницу, то увидим на ней еще box элементы, к которым также применима эта проверка. Было бы не плохо использовать одно и то же выражение для разных объектов. Попробуем изменить наше выражение на такое, подставив в него параметры objectPattern и margin:

@rule %{objectPattern} are aligned horizontally next to each other with %{margin} margin
    @forEach [${objectPattern}] as item, next as nextItem
        ${item}:
            left-of ${nextItem} ${margin}
            aligned horizontally all ${nextItem}

Теперь изменим наш тест:

@import my-rules.gspec

@objects
    menu                     #menu  .middle-wrapper
        item-*                  ul li
    box-*                    .box-container .box .panel

= Menu =
    | menu.item-* are aligned horizontally next to each other with 0 to 5px margin

= Main Section =
    | box-* are aligned horizontally next to each other with ~ 30px margin


Уже лучше. Единственное, что мне не нравится — это использовать -* символы во всех этих выражениях. Переносить их в @rule будет не разумным решением, т.к. нарушится гибкость нашей конструкции. Но конкретно в этом случае мы можем воспользоваться группировкой объектов в самом тесте. Работает это следующим образом. После объявления объектов в новой секции @groups мы можем указать группы объектов, придумать имя для каждой из групп и перечислить, какие объекты мы хотим объединить. Создадим пока что две группы menu_items и boxes.

@import my-rules.gspec

@objects
    menu                     #menu  .middle-wrapper
        item-*                  ul li
    box-*                    .box-container .box .panel

@groups
    menu_items              menu.item-*
    boxes                   box-*

Теперь, вместо поисковых выражений menu.item-* и box-* мы можем указать группы, используя символ &

= Menu =
    | &menu_items are aligned horizontally next to each other with 0 to 5px margin

= Main Section =
    | &boxes are aligned horizontally next to each other with ~ 30 px margin


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

9089d48e5b3b446d8a973fd9b2ad19b7.png

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

# ...

= Menu =
    @on desktop, tablet
        | &menu_items are aligned horizontally next to each other with 0 to 5px margin
    @on mobile
        | &menu_items are rendered in 2 column table layout, with 0px vertical and 0 to 4px horizontal margin

= Main Section =
    @on desktop, tablet
        | &boxes are aligned horizontally next to each other with ~30 px margin

    @on mobile
        | &boxes are aligned vertically above each other with 20px margin

Проверку вертикального расположения элементов для мобильной верстки боксов можно скопировать с горизонтальной и немного подправить:

# ...

@rule %{objectPattern} are aligned vertically above each other with %{margin} margin
    @forEach [${objectPattern}] as item, next as nextItem
        ${item}:
            aligned vertically all ${nextItem}
            above ${nextItem} ${margin}

А вот с табличным рендерингом элементов меню будет уже немного сложнее. Попробуем реализовать эту проверку с помощью JavaScript кода. Для этого создадим my-rules.js файл и импортируем его в файле my-rules.gspec:

@script my-rules.js

# ...

Ну, а уже в файле my-rules.js напишем нижеследующий кусок кода. Воспользуемся встроенной функцией findAll, которая находит все указанные элементы в поисковом выражении.

function _ruleRenderedInTable(rule, itemPattern, columns, verticalMargin, horizontalMargin) {
    var allItems = findAll(itemPattern);

    var currentColumn = 0;

    for (var i = 0; i < allItems.length - 1; i += 1) {
        if (currentColumn < columns - 1) {
            // генерируем проверки для горизонтального расположения двух элементов одного ряда
            rule.addObjectSpecs(allItems[i].name, [
                "left-of " + allItems[i + 1].name + " " + horizontalMargin,
                "aligned horizontally all " + allItems[i + 1].name
            ]);
        }

        var j = i + columns;

        if (j < allItems.length) {
            // проверка вертикального расположения элементов одной колонки, но разных рядов
            rule.addObjectSpecs(allItems[i].name, [
                "above " + allItems[j].name + " " + verticalMargin,
                "aligned vertically all " + allItems[j].name
            ]);
        }

        currentColumn += 1;
        if (currentColumn === columns) {
            currentColumn = 0;
        }
    }
}

rule("%{itemPattern} are rendered in %{columns: [0-9]+} column table layout, with %{verticalMargin} vertical and %{horizontalMargin} horizontal margin",
function (objectName, parameters) {
    _ruleRenderedInTable(this, parameters.itemPattern, parseInt(columns), parameters.verticalMargin, parameters.horizontalMargin);
});

Обратите внимание на синтаксис %{columns: [0-9]+}. В данном случае мы указываем собственное регулярное выражение [0-9]+ вместо стандартного .*, которое будет применятся для параметра columns. Хоть это необязательно, но таким образом мы делаем защиту от опечаток. Так как мы добавили проверки для трех разных версток, нам нужно подготовить тест-сьют, чтобы одной командой запускать тесты на всех виртуальных устройствах (desktop, tablet, mobile). Создадим файл my.test:

@@ table devices
    | tag       | size      |
    | desktop   | 1024x768  |
    | tablet    | 800x600   |
    | mobile    | 500x700   |

@@ parameterized using devices
Home page test on ${tag} device
    http://galenframework.github.io/galen-extras/website/index.html ${size}
        check homepage.gspec --include "${tag}"

Теперь запускать тесты можно с помощью команды

galen test my.test --htmlreport reports

В итоге получим такие отчеты:

2c9a1cfeb11a4b728670b9a268408f05.png

93b75220c0a4443b8f56a4bfe25340a7.png

Что дальше?

После продолжительных экспериментов с разными верстками я создал проект Galen Extras. В этом проекте я реализовал расширения для самых часто встречающихся случаев. Основной код находится в файлах galen-extras-rules.gspec и galen-extras-rules.js

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

@import galen-extras/galen-extras-rules.gspec

@objects
    header              #header .middle-wrapper
    box-*               .box-container .box .panel
    menu                #menu  .middle-wrapper
        item-*              ul li
    content             #content
    greeting            #content h1
    footer              #footer .middle-wrapper

@groups
    (box, boxes)                box-*
    (menu_item, menu_items)     menu.item-*
    skeleton_elements           header, menu, content, footer
    skeleton_element            &skeleton_elements
    image_validation            header, menu.item-*


= Skeleton =
    | &skeleton_elements sides are inside screen with 0px margin from top and bottom 
    | &skeleton_elements are aligned vertically above each other with 0 to 1px margin

    = Page is centered horizontally inside screen with 900px on desktop, but on mobile and tablet it stretches to screen =
        | every &skeleton_element is centered horizontally inside screen

        @on desktop
            | every &skeleton_element has width 900px
        @on mobile, tablet
            | every &skeleton_element is aligned vertically all screen

    = Menu items should adapt layout  =
        @on *
            | amount of visible &menu_items should be 4
            | every &menu_item is inside menu and has height ~ 64px
            | first &menu_item is inside menu 0px top left

        @on desktop, tablet
            | &menu_items are aligned horizontally next to each other with 0 to 5px margin

        @on mobile
            | &menu_items are rendered in 2 column table layout, with 0px vertical and 0 to 4px horizontal margin


= Main Section = 
    = Greeting =
        greeting:
            height 30 to 60px
            inside content 40px top, 20px left right

        @on *
            | amount of visible &boxes should be 3
            | test all &boxes with box-component.gspec

        @on desktop, tablet
            | &boxes are aligned horizontally next to each other with equal distance
            | &box sides are inside content with 20px margin from left and right
            | every &box is below greeting ~ 10px
            | every &box is above footer > 19px

        @on mobile
            | &boxes are aligned vertically above each other with 20px margin
            | every &box is inside content 20px left right
            | first &box is below greeting ~ 10px
            | last &box is above footer > 19px

Как вы можете заметить, не для всех случаев получилось написать специальные выражения. Например, в секции «Page is centered horizontally inside screen with 900 px…» мне пришлось разбить проверки на несколько подвыражений. К сожалению, написать специальное выражение для всех разновидностей конкретно этого случая мне показалось затруднительным. Получилась бы слишком длинная конструкция и, соответственно, увеличилась бы вероятность опечаток. Но если у вас появятся идеи, как расширить или улучшить все эти выражения — буду рад вашим пул реквестам.

Кстати, я также подготовил версию сайта с багами верстки:

587749edcdb043d5b65837d83cd69317.png

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

f3d243d992944259b3800a1078a4a103.png

Заключение

Как видите, если аккуратно использовать @rule фичу, можно добиться очень неплохих результатов, значительно уменьшить свой тестовый код и улучшить поддержку тестов. Также такой подход будет легче применять для TDD при разработке сайта. И что еще важно, такой код будет выступать понятной документацией, которая объясняет, где и как должен выглядеть сайт. В следующей статье я постараюсь рассмотреть Galen Framework с другой стороны и подробно рассказать о различных приемах при по-пиксельном сравнении изображений.

Ссылки

  1. Документация по Galen Extras
  2. Полная документация по языку Galen Specs
  3. Galen Framework на гитхабе

© Habrahabr.ru