Использование Selenium WebDriver для автоматического тестирования веб-интерфейса Яндекс.Почты

в 7:38, , рубрики: mocha, node.js, selenium-webdriver, Блог компании Яндекс, тестирование, тестирование веб-приложений, метки: , , ,

Без качественного тестирования невозможно разрабатывать и поддерживать крупный веб-сервис. На ранних этапах его развития часто можно обходиться только ручным тестированием по заданному тест-плану, но с появлением новых фич и увеличением количества тест-кейсов довольствоваться только им становится все сложнее и сложнее. В этой статье мы расскажем о том, как автоматизируем функциональное тестирование веб-интерфейса Яндекс.Почты с помощью Selenium WebDriver и Node.js.
Использование Selenium WebDriver для автоматического тестирования веб интерфейса Яндекс.Почты
Selenium

Помимо Selenium WebDriver существует ещё несколько решений для автоматического тестирования веб-интерфейсов, среди которых Watir, Zombie.js, PhantomJS. Но именно он стал практически стандартом. Во-первых, он имеет хорошую функциональность. А во-вторых, для него есть драйверы подо все распространённые браузеры — в том числе и мобильные — и платформы, чего не скажешь о headless-инструментах (Zombie.js, PhantomJS).

А почему именно Node.js? Потому что все фронтенд-разработчики Яндекс.Почты знают JavaScript, а именно они разрабатывают интерфейс и понимают, где и что в нём меняется от релиза к релизу.

Установка и настройка

Для установки и настройки Selenium WebDriver на локальной машине понадобятся:

  1. Java (http://www.java.com/en/download).
  2. Selenium server (скачать standalone версию можно тут — code.google.com/p/selenium/downloads/list).
  3. Node.js + npm (http://nodejs.org/download).
  4. ChromeDriver (для тестирования в Google Chrome). Качается отсюда: code.google.com/p/chromedriver/downloads/list.

После установки всех зависимостей нужно:

  1. Установить selenium-webdriver для Node.js:
    npm install selenium-webdriver -g
  2. Запустить 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();
});

По коду все довольно просто:

  1. Подключаем selenium-webdriver;
  2. Инициализируем клиент с указанием нужного браузера и передачей хоста, на котором у нас висит selenium-server;
  3. Открываем www.yandex.ru;
  4. После загрузки вводим в поисковой строке (<input name=”text”>) посимвольно “test” и кликаем на кнопку (она будет найдена по CSS-селектору ‘.b-form-button__input');
  5. Получаем тайтл страницы результатов поиска и ищем в нем подстроку ‘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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js