Помимо Selenium WebDriver существует ещё несколько решений для автоматического тестирования веб-интерфейсов, среди которых Watir, Zombie.js, PhantomJS. Но именно он стал практически стандартом. Во-первых, он имеет хорошую функциональность. А во-вторых, для него есть драйверы подо все распространённые браузеры — в том числе и мобильные — и платформы, чего не скажешь о headless-инструментах (Zombie.js, PhantomJS).
А почему именно Node.js? Потому что все фронтенд-разработчики Яндекс.Почты знают JavaScript, а именно они разрабатывают интерфейс и понимают, где и что в нём меняется от релиза к релизу.
Установка и настройка
Для установки и настройки Selenium WebDriver на локальной машине понадобятся:
- Java (http://www.java.com/en/download).
- Selenium server (скачать standalone версию можно тут — code.google.com/p/selenium/downloads/list).
- Node.js + npm (http://nodejs.org/download).
- ChromeDriver (для тестирования в Google Chrome). Качается отсюда: code.google.com/p/chromedriver/downloads/list.
После установки всех зависимостей нужно:
- Установить selenium-webdriver для Node.js:
npm install selenium-webdriver -g
- Запустить selenium server:
java -jar selenium-server-standalone-{VERSION}.jar
Первый тест
Для примера, напишем простой тест (test.js):
var wd = require('selenium-webdriver'); var assert = require('assert'); var SELENIUM_HOST = 'http://localhost:4444/wd/hub'; var URL = 'http://www.yandex.ru'; var client = new wd.Builder() .usingServer(SELENIUM_HOST) .withCapabilities({ browserName: 'firefox' }) .build(); client.get(URL).then(function() { client.findElement({ name: 'text' }).sendKeys('test'); client.findElement({ css: '.b-form-button__input' }).click(); client.getTitle().then(function(title) { assert.ok(title.indexOf('test — Яндекс: нашлось') > -1, 'Ничего не нашлось :('); }); client.quit(); });
По коду все довольно просто:
- Подключаем selenium-webdriver;
- Инициализируем клиент с указанием нужного браузера и передачей хоста, на котором у нас висит selenium-server;
- Открываем www.yandex.ru;
- После загрузки вводим в поисковой строке (
<input name=”text”>
) посимвольно “test” и кликаем на кнопку (она будет найдена по CSS-селектору ‘.b-form-button__input’); - Получаем тайтл страницы результатов поиска и ищем в нем подстроку ‘test — Яндекс: нашлось’.
Если при запуске (node test.js) никаких ошибок не произошло, то тест пройден успешно.
Все методы работы со страницей асинхронные. И, как видно на примере, каждый метод у объекта client возвращает promise-объект, на который можно навесить обработчики в случае успешного выполнения операции или возникновения ошибки (объект ошибки будет передан первым параметром).
Помимо использования промисов, для упрощения работы с API WebDriver.js имеет возможность работы в псевдосинхронном стиле, когда все вызовы методов ставятся в очередь и выполняются один за другим. Это реализовано через объект wd.promise.controlFlow(). Для контроля ошибок в таком случае используется событие “uncaughtException”:
wd.promise.controlFlow().on(‘uncaughtException’, function(e) { console.log(‘Произошла ошибка: ‘, e); });
Используя такой подход, код нашего теста можно переписать так:
wd.promise.controlFlow().on(‘uncaughtException’, function(e) { console.log(‘Произошла ошибка: ‘, e); }); client.get(URL); client.findElement({ name: 'text' }).sendKeys('test'); client.findElement({ css: '.b-form-button__input' }).click(); client.getTitle().then(function(title) { assert.ok(title.indexOf('test — Яндекс: нашлось') > -1, 'Ничего не нашлось :('); }); client.quit();
Возможные проблемы и ошибки
Но даже при выполнении такого, казалось бы, простого теста могут возникнуть разные ошибки. Мы расскажем о наиболее частых из тех, с которыми у вас есть вероятность столкнуться, и о том, как их предотвращать.
Разные версии клиента и сервера
Чтобы этого избежать, перед запуском теста необходимо убедиться, что ваша версия WebDriver.js совпадает с версией selenium-server. Иначе вы не сможете полноценно использовать все новые функции, а в некоторых случаях новые браузеры могут вообще не запуститься.
Используемый в тесте элемент не находится через findElement
У этого может быть несколько причин. Во-первых, нужно убедиться, что элемент действительно присутствует на странице, и, если вы используете css-селекторы, селектор матчится на элемент. Это вроде бы очевидно, но в условиях частого обновления верстки — как, например, в Яндекс.Почте — нужно следить за актуальностью селекторов в тестах. Чтобы немного упростить этот процесс, можно для всех ключевых элементов теста давать дополнительный класс (например, t-login-button) и договориться никогда не трогать такие классы при внесении изменений в верстку.
Во-вторых, элемент может появиться на странице динамически — во время выполнения каких-либо действий. Например, при смене значения свойства display. До этого момента, при попытке работы с такими элементами будет выведена ошибка: «Element is not currently visible and so may not be interacted with». Элемент считается невидимым, если для него выполняется хотя бы одно из перечисленных условий:
- значение свойства display равно none;
- значение свойства visibility равно hidden;
- значение свойства opacity равно 0 (кроме операции клика);
- значение атрибута type равно hidden (если это input);
offsetWidth и offsetHeight равны нулю.
В-третих, элемента еще может не быть в DOM на момент выполнения операции. По умолчанию WebDriver посылает команды браузеру без каких-либо таймаутов. Часто возникают ситуации, когда браузер еще не успел закончить рендерить результат предыдущего действия, а WebDriver уже посылает команду на поиск нового элемента. Здесь есть несколько способов решения проблемы. Самый простой — но в то же время и самый плохой — перед каждым поиском элементов вставлять таймаут через:
client.sleep(<количество миллисекунд>)
Второй способ лучше — можно установить дефолтный таймаут на поиск элементов через:
client.manage().timeouts().implicitlyWait(<количество миллисекунд>);
Третий и самый хороший, на наш взгляд, вариант решения этой проблемы — поиск элемента с поллингом раз в 50 миллисекунд через метод isElementPresent:
var locator = { css: ‘.b-button’ }; client.isElementPresent(locator).then(function(found) { if (found) { client.findElement(locator).click(); } });
Есть и четвёртый вариант. Можно выполнять любой код по наступлению некоторого условия через метод wait:
client.wait(function() { return client.findElement({ css: ‘.b-button’ }); }, <таймаут в миллисекундах>).then(function() { // Нашли кнопку // ... });
Тест выполняется корректно, но иногда очень сильно тормозит
Чаще всего это связано с особенностью работы метода get. WebDriver считает, что страница загрузилась только тогда, когда произошло событие load, а оно, как известно, наступает после загрузки всех ресурсов страницы. То есть на время выполнения теста могут влиять разные сторонние ресурсы, которые используются на странице (например, счетчики, социальные кнопки и другие виджеты). К сожалению, поменять событие, которого ждет get в текущей версии WebDriver невозможно, но можно установить таймаут на время общей загрузки страницы:
client.manage().timeouts().pageLoadTimeout(<время в миллисекундах>);
Еще можно задать общий таймаут на ожидание команды сервером:
java -jar selenium-server-standalone-{VERSION}.jar -browserTimeout=<время в секундах>
Более сложные примеры
Ранее мы рассмотрели достаточно простой пример, который показывает лишь небольшую часть возможностей WebDriver. Помимо загрузки страниц, поиска элементов по локаторам и работы с формами, WebDriver позволяет выполнять и более продвинутые действия.
Работа с алертами и фреймами:
// получение текста алерта client.switchTo().alert().getText(); // ввод текста в prompt client.switchTo().alert().sendKeys(‘name’); // отмена client.switchTo().alert().dismiss(); // переключение на определенный фрейм client.switchTo().frame(‘frame-id’) client.findElement({ css: ‘.b-another-button’ }).click();
Выполнение произвольного JS-кода:
client.executeAsyncScript(function() { var cb = arguments[arguments.length - 1]; $.getJSON(‘/suggest.json’, { query: ‘pa’ }, function(data) { cb(data); }); }).then(function(data) { var contacts = JSON.parse(data).contacts; console.log(contacts[0]); });
В том числе и синхронно:
client.executeScript(‘window.scrollTo(0, 500)’);
Эмуляция драг-н-дропа и нажатие нескольких клавиш одновременно:
var message = client.findElment(‘message’); var anotherMessage = client.findElement(‘another-message’); var dropZone = client.findElement(‘drop’); var action = wd.ActionSequence(client) .keyDown(wd.Key.SHIFT) .click(message) .click(anotherMessage) .dragAndDrop(dropZone) .keyUp(wd.Key.SHIFT); // … // вызов экшена action.perform();
Подробнее про возможности WebDriver можно почитать на docs.seleniumhq.org/docs/03_webdriver.jsp, а также в исходниках WebDriver.js.
Запуск нескольких тестов с помощью Mocha
До этого мы рассматривали запуск и проверку только одного теста в одном браузере, что, согласитесь, не очень удобно. Для того чтобы автоматизировать запуск нескольких тест-кейсов и упростить написание тестов под Node.js, есть несколько библиотек. Нам пришлась по душе библиотека Mocha от T. J. Holowaychuk. Почему именно она? Потому что не навязывает какой-то конкретный стиль написания тестов и позволяет использовать любую библиотеку для ассертов. Также она имеет большое число репортеров в разных представлениях и форматах.
Простейщий тест-кейс с использованием Mocha и Chai (библиотека для ассертов) выглядит так:
var assert = require('chai').assert; var webdriver = require('selenium-webdriver'); var config = require('../config'); var client = new webdriver.Builder() .usingServer('http://' + config.selenium.host + ':' + config.selenium.port + '/wd/hub') .withCapabilities({ 'browserName': config.browsers[0] }).build(); client.manage().timeouts().implicitlyWait(config.WAIT_TIMEOUT); suite('Общее'); test('Загружаем http://www.yandex.ru', function(done) { client.get(‘http://www.yandex.ru’); client.isElementPresent({ css: '.b-morda-search-decor' }).then(function(result) { assert.isTrue(result); done(); }, done); }); test('Загружаем http://yandex.ru/yandsearch?text=test', function(done) { client.get(‘http://yandex.ru/yandsearch?text=test’); client.isElementPresent({ css: '.b-serp-list’ }).then(function(result) { assert.isTrue(result); done(); }, done); }); after(function(done) { client.quit().then(done); });
Так как все операции асинхронные, то и тесты должны быть асинхронными. В Mocha работа с ними сделана очень удобно. Чтобы указать, что тест асинхронный, нужно просто передать в функцию теста первым параметром колбек (в нашем случае done) и вызвать его, когда тест завершился. В случае неудачного завершения, в колбек передается объект ошибки.
Запускается тест-кейс так:
mocha --reporter spec --ui qunit --timeout 1200000 --slow 10000 test.js
Обратите внимание, что, помимо репортера и интерфейса для написания тестов, мы указали таймаут в 20 секунд и порог, по которому Mocha определяет, что тест выполнился медленно. Таймаут нужно увеличить потому, что в общем случае Mocha используется для синхронных юнит-тестов и дефолтный таймаут равен двум секундам. В случае же с нашими тестами этого времени часто не хватает даже на открытие браузера. Чтобы не писать каждый раз такой длинный список флагов, можно сохранить их в файл mocha.opts (каждый флаг со значением на новой строке) и положить в папку с тестами (mocha по умолчанию выполняет все .js файлы из папки test). Тогда запуcкать выполнение тестов можно просто по mocha.
Параллельное выполнение тестов
С ростом количества тестов и увеличением их сложности время выполнения всего тестового плана даже в одном браузере может достигать нескольких минут. Одной из затратных операций является открытие браузера, поэтому для ускорения имеет смысл выполнять серию тестов в рамках одной сессии. Этого можно добиться с помощью создания нового потока выполнения — на самом деле, нового окна браузера — через метод wd.promise.createFlow. Перепишем наш первый тест на несколько параллельных запросов к поиску с ожиданием результатов:
var wd = require('selenium-webdriver'); var assert = require('assert'); var SELENIUM_HOST = 'http://localhost:4444/wd/hub'; var URL = 'http://www.yandex.ru'; var WAIT_TIMEOUT = 500; var queries = ['test', 'webdriver', 'node.js']; var flows = queries.map(function(query) { return wd.promise.createFlow(function() { var client = new wd.Builder() .usingServer(SELENIUM_HOST) .withCapabilities({ browserName: 'firefox' }) .build(); client.manage().timeouts().implicitlyWait(WAIT_TIMEOUT); client.get(URL); client.findElement({ name: 'text' }).sendKeys(query); client.findElement({ css: '.b-form-button__input' }).click(); client.getTitle().then(function(title) { assert.ok(title.indexOf(query + ' — Яндекс: нашлось') > -1, 'Ничего не нашлось :('); }); client.quit(); }); }); wd.promise.fullyResolved(flows).then(function() { console.log('Все ок!'); });
Аналогичным способом можно открывать несколько браузеров сразу, меняя browserName при инициализации нового клиента. Но для более эффективного запуска тестов в разных браузерах и на разном количестве машин есть отдельный инструмент, который называется Selenium Grid. Он позволяет поднимать несколько selenium-server’ов на разных портах или разных машинах и управлять их конфигурацией. Таким образом, можно коннектиться к общему хабу серверов, и тесты сами будут выполняться на всех свободных серверах параллельно. Для настройки хаба нужно выполнить:
java -jar selenium-server-standalone-{VERSION}.jar -role hub
После этого по адресу localhost:4444/grid/ будет доступна консоль управления, где можно смотреть зарегистрированные сервера и управлять ими. Для регистрации сервера в хабе, выполните:
java -jar selenium-server-standalone-{VERSION}.jar -role webdriver -hub http://localhost:4444/grid/register -port <port>
Полезные ссылки
docs.seleniumhq.org — документация по Selenium.
code.google.com/p/selenium/wiki/WebDriverJs — документация по WebDriver.js.
dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html — черновик спецификации WebDriver API.
ссылка на оригинал статьи http://habrahabr.ru/company/yandex/blog/173769/
Добавить комментарий