Pull to refresh

Тестируем в браузере с помощью Geb

Reading time 10 min
Views 14K

Geb на практике


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

Browser.drive(driver: new InternetExplorerDriver()) {

    go "http://www.google.com"

    $('form', action:endsWith('/search')).q = 
                    'тестирование при помощи geb и spock'
    $('button', value:'Поиск').click()
    waitFor { $('#search') }
    assert $('#search').size() == 1
    assert $('#search').find('li.g a.l').size() > 0
    println "Первый результат: " + $('#res').find('li.g a.l', 0).text()
}.quit()

Мне кажется, у таких тестов высокая степень читаемости — неважно даже, какой это язык. Можно по такому вот образцу написать еще несколько подобных же тестов, не имея вообще никакого понятия о Geb, Groovy и о том, как это работает. Но для полного понимания немного углубимся в основы.

Про Geb и Selenium


Кто в наши дни не слышал про Selenium? Наверное, тот, кто ещё пороху не нюхал и до сих пор думает, что все браузеры одинаковы. Долго я про это распространяться не буду, достаточно лишь знать:
  • Selenium позволяет запускать автоматические тесты прямо в браузере, воспроизводя всевозможные глюки и особенности.
  • Можно делать ВСЁ то, что делает обычный пользователь, а именно набирать текст, скроллить, двигать мышкой и т.п. При этом внимательно наблюдая за поведением страницы в браузере.
Почему всё это так актуально? В современных веб-приложениях неимоверное количество JavaScript-кода, спецэффектов, анимации, AJAX и прочих прелестей. Никаких нормальных инструментов, позволяющих контролировать качество без браузера в общем-то и нет.

Фактически Selenium — это полноценный робот для тестирования в условиях, максимально приближенных к реальности. (Исторически можно еще припомнить Rational Robot, который был заточен под Internet Explorer и имел довольно скудные возможности по взаимодействию со страницей.)

Сам Selenium предоставляет несколько вариантов написания тестов:
  1. С помощью JavaScript. Тесты запускаются прямо в браузере и имеют прямой доступ к загруженной веб-странице, что позволяет им совершать любые манипуляции.
  2. Через управляющий сервер. В отдельном процессе запускается тестовый код на произвольном языке. Этот код вызывает специальную программу — WebDriver — которая передает команды браузеру и позволяет контролировать состояние страницы.
Далеко не все любят писать тесты на JavaScript, поэтому второй способ — WebDriver — получает все большее распространение. Он пригоден для разработки тестов на любом языке, удобен еще и тем, что позволяет выполнять тесты на другой машине.

Итак, Geb — это очередное средство написания тестов, использующее WebDriver и основанное на языке Groovy. Использовать Geb внутри Groovy-скрипта можно с помощью следующих волшебных слов:

@Grab(group='org.codehaus.geb', module='geb-core', version='0.6.2')
@Grab(group='org.seleniumhq.selenium', module='selenium-api', version='2.14.0') 
@Grab(group='org.seleniumhq.selenium', module='selenium-firefox-driver', version='2.19.0')
@Grab(group='org.seleniumhq.selenium', module='selenium-ie-driver', version='2.19.0')

import geb.Browser
import org.openqa.selenium.firefox.FirefoxDriver
...


Язык Geb


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

В нашем случае Geb предоставляет язык управления браузером. Можно совершать все, что умеет человеческий пользователь:
  • Переходить по URL
  • Заполнять формы
  • Двигать мышкой
  • Кликать по ссылке
  • Взаимодействовать с разного рода popup-окнами и фреймами.
Пример:

Browser.drive {
    go "http://www.gramant.ru"

    $('.block .caption', text: 'Grails')
        .closest('.block')
        .find('a', text: startsWith('Подробнее'))
        .click()
    assert $('title', text:'Grails | Gramant').size() == 1
}

Как видим, Geb позволяет использовать синтаксис, напоминающий jQuery (но не совпадающий с ним полностью). Это удобно для нахождения нужных элементов в странице. Метод click() позволяет кликать на DOM-элементы, операция << — отправлять браузеру текст, и так далее.

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

Страницы и модули


Мы скомандовали
go 'http://www.gramant.com'
и перешли на страницу gramant.com. Теперь можно эту страницу «прочитать», т.е. проанализировать и найти ошибки. Так? На самом деле нет. При попадании браузера на страницу может случится:
  • HTTP redirect
  • Сработает какой-нибудь загадочный javascript-код и перенаправит вас на другую страницу.
Мы думаем, что находимся на одной странице, а оказались на другой. Дальше тесты пойдут неправильным путем. Поэтому очень важно понимать, на какой логической странице мы находимся.

Geb предлагает использовать логические страницы (Page) и так называемые модули (Module). Попытаемся это проиллюстрировать следующим примером:


Зачем вообще нужна такая абстракция, как страница? Казалось бы, достаточно посмотреть текущий URL и мы все узнаем. Например, так:

class SearchResultsPage extends Page {
    static url = "/yandsearch"
}

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

class SearchResultsPage extends Page {
    static at = {
        $('title').text() ==~ '.*Яндекс: Нашлось .* ответов'
    }
}

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

Несколько слов про модули. На диаграмме видно, что модуль — это часть страницы (блок), причем есть модули, которые присутствуют на нескольких страницах. Например, поисковая строка Яндекса доступна как на портальной странице, так и над результатами поиска. Смысл модулей — повторное использование тестирующего кода.

Модули можно определить, например, так:

class LoginModule extends Module {
    static content = {
        username {}
        password {}
        loginButton { $("input", type: "submit") }
    }
}

И затем использовать их таким образом:

class HomePage extends Page {
    static content = {
        login { module LoginModule }
    }
}

Browser.drive {
    to HomePage
    login.username << 'user'
    login.password << 'password'
    login.loginButton.click()
}

Существует, разумеется, возможность, определять много экземпляров модуля внутри страницы — это актуально для разного рода списков. Например, результаты поиска — каждый результат можно объявить модулем. Это делается с использованием конструкции moduleList, на которой я останавливаться не буду.

AJAX и все, все, все


В теперешние времена внутри браузера может происходить множество удивительных вещей:
  • AJAX-запросы
  • Анимация
  • Drag & Drop
  • Popup-окна разного вида
Тестировать такие штуки автоматически можно только при помощи in-browser тестов. Geb предлагает несколько инструментов:

Доступ к переменным JavaScript

Через объект js можно обратиться к значению глобальных переменных JavaScript:

Browser.drive {
    assert js.myGlobalVar == 1
}

Можно также пользоваться глобальными функциями:

Browser.drive {
    js.globalCall()
    assert js.globalFunc() == 1
    js."document.write"("go geb!")
    js.exec("return document.location.href") == 'http://www.gramant.ru'
}

Этим механизмом я пользовался мало, поэтому предполагаю, что Geb будет стараться правильно сконвертировать все типы данных JavaScript (как это описано здесь) в Groovy-типы, но зуб не даю.

Для эмуляции мышиных событий удобно использовать jQuery. Geb предоставляет специальное свойство jQuery для такого рода вызовов:

Browser.drive {
    $("div#a").jquery.mouseover()
}

Эта строчка будет работать только при условии, что на тестируемой странице загружена jQuery версии 1.4 и выше. Фактически такой код транслируется в js.exec().

Условие ожидания

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

Browser.driver {
    go 'http://www.youtube.com/watch?v=8d1hp8n1stA'
    $('button#watch-share').click()
    waitFor { $('#watch-actions-share').displayed }
    $('#watch-actions-share').find('button.share-panel-embed').click()
    waitFor { $('textarea.share-embed-code').displayed }
    println "Embed code для этого видео: " + $('textarea.share-embed-code').value()
}

Этот скрипт получает embed code для ролика YouTube, нажимая несколько кнопок и ожидая завершения анимации.

Разумеется, вечно метод waitFor ждать не будет и по достижении некоторого таймаута рухнет, прервав тест. По умолчанию это 5 секунд.

Использование WebElement и Actions

Мы упомянули про Drag & Drop. На данный момент (версия 0.6.2) Geb не имеет удобной абстракции для осуществления таких операций. Тем не менее, всегда есть возможность использовать Selenium напрямую, обратившись к экземпляру WebDriver (через browser.driver):

WebElement underlyingElement = $('#myElement').getElement(0)
 
Action action = new Actions(browser.driver)
    .clickAndHold(underlyingElement)
    .moveByOffset(15,15)
    .release()
    .build()
 
action.perform()

Класс Actions — это низкоуровневый способ создать последовательность браузерных действий с тем, чтобы их последовательно выполнить методом perform(). Обращаю внимание, что Actions по своему функционалу мощнее, чем просто вызовы JavaScript-кода и далеко не все действия пользователя можно имитировать через JavaScript. Реализация Actions на стороне браузера сильно зависит от того, какой драйвер используется и не является переносимой. Использование Actions — практически единственный вариант для генерации tap-событий в планшетах с touch screen. Есть и ограничения — Actions не позволяет вам совершать манипуляции с Flash-компонентами на странице; это возможно только через ExternalInterface при помощи JavaScript, что сильно сложнее.

Geb внутри Grails


Для использования Geb в Grails удобно применять плагин Spock. Я не буду долго распространяться про Spock Framework, информации о нем достаточно. Spock удобнее стандартного JUnit прежде всего тем, что позволяет писать тесты на своем мета-языке спецификаций (опять-таки на базе Groovy). Это получается и короче, и выразительнее.

Несколько слов о настройке Geb для Grails 1.3.x. Пример такого проекта выложен здесь. Нужная секция в BuildConfig.groovy будет выглядеть так:

dependencies {
    test("org.seleniumhq.selenium:selenium-htmlunitdriver:$seleniumVersion") {
        exclude "xml-apis"
    }
    test("org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion")
    test("org.seleniumhq.selenium:selenium-firefox-driver:$seleniumVersion")

    test "org.codehaus.geb:geb-spock:$gebVersion"
}
plugins {
    test ":tomcat:$grailsVersion"
    test ":hibernate:$grailsVersion"

    test ":geb:$gebVersion"
    test ":spock:0.5-groovy-1.7"
}

Текущий gebVersion — 0.6.2, подставьте нужный.

Мы знаем, что в Grails есть два типа тестов — test/unit и test/integration. Spock добавляет еще и test/functional — «функциональные» тесты.

Geb внутри функциональных тестов (спецификаций Spock) похож на обыкновенный, но запускать браузер и конфигурировать драйвер внутри теста не нужно — все уже сделано. Пример простенькой спецификации (предполагаем, что классы HomePage, LoginPage нами уже описаны):

@Stepwise
class CoreSpec extends GebReportingSpec {

    def "unauthorized user goes to login page"() {
        when:
        to HomePage
        then:
        at LoginPage
    }

}

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

Конечно, есть еще файлы настройки Geb, которые определяют:
  • Какой браузер использовать и с каким драйвером
  • Разнообразные настройки Geb (такие как таймаут).
В демонстрационном примере есть пример файла GebConfig.groovy.

Вроде бы этого достаточно. Набираем:

grails -Dgeb.env=firefox test-app :spock

и смотрим, как наш робот пытается что-то там тестировать.

Быстро выясняется, что на Chrome и IE наши тесты не работают. Причина в том, что реально с нуля и без подручных средств можно запустить либо HtmlUnit (который для тестирования бесполезен) либо Firefox. Для Firefox автоматически создается новый профиль, который «расширяет» браузер для последующего управления через FirefoxDriver. Да, Selenium/Geb замечательно работает на Firefox, потому что именно для Firefox он изначально и разрабатывался.

Что касается IE и Chrome, то с ними все несколько сложнее. Общую ситуацию можно понять из таблички:
Браузер Что потребуется Ограничения
firefox Firefox Под некоторыми OS подвержен проблеме исчерпания TCP-портов.
 Chrome Потребуется скачать и установить отдельный платформо-зависимый сервис (ChromeDriverService). Отсутствует поддержка Actions.
 Opera Не ниже версии 11.5, только один экземпляр браузера единовременно.
 Internet Explorer Требует запуска отдельного сервиса. Требует предварительной настройки браузера (Protection Mode). Версии 6,7,8,9. Поддерживает ровно один экземпляр браузера.
Как видим, драйвер Chrome не поддерживает Actions! Что довольно неприятно.

Что еще официально поддерживает Selenium (а, следовательно, Geb)?
  • Браузер iPhone
  • Браузер Android

Писать ли браузерные тесты?


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

Однако браузерные тесты:
  • Работают медленно (гораздо медленнее чем любой unit-тест) — просто в силу того, что скорость реакции браузера ограничена;
  • При тестировании сложной JavaScript/AJAX-начинки — трудны в написании;
  • Требуют поддержки. Любое изменение CSS-классов и иерархии страницы может сломать тесты. Причем, как правило, число таких вот мелких визуальных изменений сильно превышает число изменений, скажем, в схеме БД или бизнес-логике.
Простейшая формула, описывающая необходимость написания автоматических тестов, может выглядеть так:



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

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

Вообще, это может звучать непривычно, но автоматический тест — это программа. Следовательно, иногда её разработку и поддержку можно поручить программистам. Этот подход нивелирует строгое разделение труда между программистом и тестировщиком. В рамках налаженной системы сборки и тестирования часть применяют принцип «сам сломал тест, сам и чини». То есть, грубо говоря, неважно, кто тест написал — с момента создания он становится общей собственностью и ответственность за работу теста несут как тестировщики, так и программисты.

Отдельно стоит упомянуть про так называемый recording — возможность записывать ваши действия в браузере и на их основе создавать тесты. Эту возможность предоставляет Selenium IDE в Firefox. Звучит это здорово, однако созданные таким образом тесты обычно обладают низкой устойчивостью, то есть вероятность их поломки при какой-то изменения страницы весьма велика. Происходит это потому, что recording не знает, как правильно адресовать блоки страницы, с которыми вы взаимодействуете — нужно ли использовать ли CSS-классы, #id или какие-то иные способы. Кроме того, логику (начинку) тестов Selenium за вас все равно придумать не сможет.

Резюмирую: Geb — приятный в использовании и вполне работающий продукт (не особо обращаем внимание на номер версии, ибо стабильность работы Geb обеспечивает Selenium), вполне годный к написанию браузерных тестов для Grails-приложений. Оставляю вас с ним наедине: http://gebish.org.
Tags:
Hubs:
+5
Comments 2
Comments Comments 2

Articles