Без качественного тестирования невозможно разрабатывать и поддерживать крупный веб-сервис. На ранних этапах его развития часто можно обходиться только ручным тестированием по заданному тест-плану, но с появлением новых фич и увеличением количества тест-кейсов довольствоваться только им становится все сложнее и сложнее. В этой статье мы расскажем о том, как автоматизируем функциональное тестирование веб-интерфейса Яндекс.Почты с помощью Selenium WebDriver и Node.js.
Помимо 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.
Автор: Panya