Функціональне тестування сучасних web-додатків



Сучасні web-додатки найчастіше містять безліч "рухомих частин" і сторонніх залежностей. В процесі рефакторінгу і додавання/зміни функціональності в такому додатку може відбутися поломка існуючих use-case сценаріїв і нестабільна робота в певних браузерах.
Для своєчасного виявлення таких ситуацій та виконання безперервної інтеграції необхідно функціональне тестування web-додатки. У статті піде мова про двох безкоштовних open-source рішеннях:
Розглянуті рішення забезпечують схожий, на перший погляд, ряд можливостей.
  • Автоматизоване функціональне тестування з послідовним і паралельним виконанням тестів та їх угрупованням, інтеграція з Gulp і Mocha.
  • Підтримка більшості десктопних і мобільних браузерів; виконання взаємодії з реальним DOM API (web-додаток відкривається в нескрытом вікні браузера у звичайній вкладці — тест максимально наближений до життя).
  • Широка підтримка селекторів елементів сторінки: document.querySelector, CSS-селектори і навіть XPath; комбінація цих варіантів дозволяє зберегти працездатність функціонального тесту при внесенні змін до розмітку.
  • Автоматичне очікування готовності DOM-моделі браузера, синхронна подача команд візуального взаємодії (клацання по кнопках, введення в текстові поля); зручні інструменти для очікування клієнтського JS-коду і мережевий AJAX-активності.
  • Підтримка iframe-ов — за рахунок вибору поточного контексту DOMWindow — вікна для тіста і виконання команд у його межах з можливістю перемикання в будь-який момент.
  • Можливості для розширення — обидва рішення надають можливості по додаванню підтримки кастомних браузерів і розширення власної функціональності, в тому числі додавання нових команд.
Приклад функціонального тесту
Прикладом для тестування буде web-додаток виду TodoMVC, з сервером node.js і клієнтської SPA-сторінкою на React+Redux-е. Для наближення умов тестування до реальних, у всі redux-івські action-и додані випадкові затримки, эмулирующие мережеве взаємодія з backend-му. (За основу взято http://todomvc.com/examples/react/
Надалі буде передбачатися, що тестове web-додаток запущено за адресою http://localhost:4000/. Функціональний тест буде простим і включати в себе додавання todo-елемента, правку його вмісту, відмітку як виконане/невиконане завдання і видалення.
Мовою для написання тестів в обох фреймворках є JS (ES2016 і ES5 відповідно для TestCafe і Nightwatch), однак це чудово підходить для web-додатків, написаних на будь-якій мові. Якщо Ви давно не розробляли на JS, то необхідно враховувати, що сучасні редакції пішли дуже далеко від старого ES3, і включають зручні засоби для написання коду, об'єктно-орієнтованого та функціонального програмування і багато іншого.
Установка TestCafe виробляється лише однією командою
npm install -g testcafe
. Після завантаження та установки необхідних залежностей, виконання тесту здійснюється командою
testcafe <browser-name> <tests-directory>/
у відповідній директорії.
Вихідний код функціонального тесту, перевіряючого викладений вище use-case-сценарій, може бути таким:
import { expect } from 'chai';
import { Selector } from 'testcafe';

const MAX_TIME_AJAX_WAIT = 2500; // 2.5 seconds by default
const querySelector = Selector((val) => document.querySelector(val), {
timeout: MAX_TIME_AJAX_WAIT
});
const querySelectorCondition = Selector((val, checkFunc) => {
const foundElems = document.querySelectorAll(val);
if(!foundElems) return null;
for(let i=0; i < foundElems.length; i++) {
if(checkFunc(foundElems[i])) return foundElems[i];
}
return null;
}, {
timeout: MAX_TIME_AJAX_WAIT
});

fixture `Example page`
.page `http://localhost:4000/`;

test('Emulate user actions and perform a verification', async t => {
await t.setNativeDialogHandler(() => 'true');
const inputNewTodo = await querySelector('header.header input.new-todo');
await t.typeText(inputNewTodo, 'New TODO element\r\n');

const addedTodoElement = await querySelectorCondition(
'section.main label',
(elm) => (elm.innerText === 'New TODO element')
);

await t.doubleClick(addedTodoElement);

const addedTodoEditInput = await querySelectorCondition(
'section.main input[type=text]',
(elm) => (elm.value === 'New TODO element')
);

await t.typeText(addedTodoEditInput, 'changed\r\n');

const addedTodoCheckboxAC = await querySelectorCondition(
'section.main input[type=checkbox]:not([checked])',
(elm) => (elm.nextSibling.innerText === 'New TODO element changed')
);

await t.click(addedTodoCheckboxAC);

const addedTodoCheckboxBC = await querySelectorCondition(
'section.main input[type=checkbox]',
(elm) => (elm.nextSibling.innerText === 'New TODO element changed')
);

await t.click(addedTodoCheckboxBC);

const addedTodoDelBtn = await querySelectorCondition(
'button.destroy',
(elm) => (elm.previousSibling.innerText === 'New TODO element changed')
);

await t.click(addedTodoDelBtn);
});

Адреса досліджуваної web-сторінки визначається fixture-частини, за якими слідують функціональні тести, по завершенні кожного з яких web-сторінка автоматично відновлюється в початковий стан. Для пошуку DOM-елементів на сторінці використовуються testcafe-специфічні Selector-и, використовуються в якості обгортки для функції, яка буде виконувати запит до DOM-моделі, можливо використовуючи аргументи.
У цьому прикладі перший селектор являє обгортку над document.querySelector, а другий — над document.querySelectorAll з callback-функції, що допомагає вибрати потрібний елемент зі списку. Обгортка Selector приймає опції, тут зокрема встановлюється максимальний час, протягом якого testcafe буде очікує поява елемента з заданими характеристиками в DOM-моделі.
Сам функціональний тест являє собою набір асинхронних викликів Selector-ів, між якими виробляються дії допомогою test controller-а, инстанцированного змінної t. Призначення більшості його методів очевидно з назв (click, typeText тощо), а
t.setNativeDialogHandler
використовується для запобігання генерації alert-подібних вікон, які можуть «повісити» тест — що дуже зручно.
Установка Nightwatch теж починається з простої команди
npm install -g nightwatch
, однак для запуску функціональних тестів ще потрібно Selenuim-server (Можна завантажити тут http://selenium-release.storage.googleapis.com/3.0/selenium-server-standalone-3.0.1.jarна комп'ютері повинен бути встановлений Java SE runtime) і webdriver-s для кожного із браузерів, в яких буде запускатися тест.
Для запуску тестів спочатку треба створити конфігураційний файл nightwatch.json, що описує розташування тестів, шляхи і налаштування для selenium-server і webdriver-ов, а далі використовувати просту команду
nightwatch
в поточній директорії.
Якщо використовувати Microsoft Web Driver (Edge), то nightwatch.json може виглядати приблизно так:
{
"src_folders" : ["nightwatch-tests"],
"output_folder" : "reports",
"custom_commands_path" : "",
"custom_assertions_path" : "",
"page_objects_path" : "",
"globals_path" : "",
"selenium" : {
"start_process" : true,
"server_path" : "nightwatch-bin/selenium-server-standalone-3.0.1.jar",
"log_path" : "",
"порту" :4444,
"cli_args" : {
"webdriver.edge.driver" : "nightwatch-bin/MicrosoftWebDriver.exe"
}
},
"test_settings" : {
"default" : {
"selenium_port" : 4444,
"selenium_host" : "localhost",
"desiredCapabilities": {
"browserName": "MicrosoftEdge",
"acceptSslCerts": true
}
}
}
}

Вихідний код функціонального тесту, перевіряючий аналогічний розглянутому вище use-case-сценарій, може виглядати так:
module.exports = {
'Demo test' : function (client) {
client.url('http://localhost:4000/')
.waitForElementVisible('body', 1000)

// May use CSS selectors for element search
.waitForElementVisible('header.header input.new-todo', 1000)
.setValue('header.header input.new-todo', ['New TODO element', client.Keys.ENTER])

// Or use Xpath - it's more powerful tool
.useXpath()
.waitForElementVisible('//section[@class=\'main\']//label[text()=\'New TODO element\']', 2000)
.execute(function() {
// For dispatch double click - Nightwatch doesn't support it by default
var evt = new MouseEvent('dblclick', {'view': window, 'bubbles': true,'cancelable': true});
var foundElems = document.querySelectorAll('section.main label');

if(!foundElems) return;
var elm = null;

for(var i=0; i < foundElems.length; i++) {
if(foundElems[i].innerText === 'New TODO element') {
elm = foundElems[i];
break;
}
}

elm.dispatchEvent(evt);
})

.waitForElementVisible('//section[@class=\'main\']//input[@type=\'text\']', 2000)
.clearValue('//section[@class=\'main\']//input[@type=\'text\']')
.setValue('//section[@class=\'main\']//input[@type=\'text\']',
['New TODO element changed', client.Keys.ENTER]
)

.waitForElementVisible(
'//section[@class=\'main\']//label[text()=\'New TODO element changed\']' +
'/preceding-sibling::input[@type=\'checkbox\' and not(@checked)]',
2000
)
.click(
'//section[@class=\'main\']//label[text()=\'New TODO element changed\']' +
'/preceding-sibling::input[@type=\'checkbox\' and not(@checked)]'
)

.waitForElementVisible(
'//section[@class=\'main\']//label[text()=\'New TODO element changed\']' +
'/preceding-sibling::input[@type=\'checkbox\']',
2000
)
.click(
'//section[@class=\'main\']//label[text()=\'New TODO element changed\']' +
'/preceding-sibling::input[@type=\'checkbox\']'
)

.waitForElementVisible(
'//section[@class=\'main\']//label[text()=\'New TODO element changed\']' +
'/following-sibling::button[@class=\'destroy\']',
2000
)
.click(
'//section[@class=\'main\']//label[text()=\'New TODO element changed\']' +
'/following-sibling::button[@class=\'destroy\']'
)

.waitForElementNotPresent(
'//section[@class=\'main\']//label[text()=\'New TODO element changed\']',
2000
)

.pause(2000)
.end();
}
}

Серед особливостей коду легко помітити, що Nightwatch не підтримує емуляцію подвійного клацання по елементу — замість цього доводиться реалізовувати обхідний шлях з inject-функцією на клієнта, виконує dispatchEvent на цільовому елементі управління.
Зручною можливістю Nightwatch є підтримка XPath, який надає значно ширші можливості щодо селекції елементів DOM-моделі, порівняно з CSS-селекторами — взяти хоча б вилучення елемента за його текстовим наповненням, що досить часто зустрічається у функціональному тесті.
Порівняння функціональності TestCafe і Nightwatch
Установка:
[T]estCafe: Досить однієї команди
npm install -g testcafe
, і можна відразу приступати до написання та запуску тестів в поточній директорії web-проекту
[N]ightwatch: Хоча установка npm-модуля робиться просто —
npm install -g nightwatch
, потрібно мануальна завантаження selenium-standalone-server, webdriver-ів для всіх вакансій браузерів, ручне створення конфігураційного файлу (А може навіть завантаження Java, якщо вона не встановлена)
Вибірка DOM-елементів:
T: Використовується механізм Selector-ов — обгорток над клієнтськими JS-функціями, що вибирають один або безліч DOM-вузлів; це забезпечує практично необмежені можливості для селекції, починаючи від простої обгортки над
document.querySelector
або
document.getElementById
, і закінчуючи довільній логікою проходу по DOM-елементів. При цьому TestCafe самостійно забезпечує перевірку наявності/відсутності елементів по заданому Selector-у протягом зазначеного часу.
N: На вибір надається CSS selectors або XPath — в принципі, другого варіанту достатньо для вирішення більшості завдань по вибірці елементів на сторінці, хоча це звичайно не зрівняється з можливістю завдання складною довільній логіки пошуку.
Мова написання тестів:
T: З коробки надається можливість написання тестів безпосередньо мовою ES2016, що дозволяє писати простий і читабельний код тестів на тому ж мовою, що і сама web-додаток Це також зручно у випадках, коли потрібно імпортувати певний модуль з досліджуваного проекту.
N: Застарілий ES5-синтаксис і exports-конструкції у вихідному коді функціонального тесту (Можливість прикрутити ES6 все-таки є, але на милицях)
Підтримка асинхронних операцій:
T: Всі API-функції засновані на Promise-ах, що дозволяє описувати довільну асинхронну логіку роботи тесту, і при цьому інтегрувати власні функції з боку node.js. Завдяки підтримці ES2016, цей код можна записувати за допомогою async і await конструкцій в послідовному стилі.
N: У тесті можна реалізувати послідовність команд з аналізу та управління вмістом web-сторінки, однак вони складаються у внутрішню чергу подій, і інтегрувати власні асинхронні функції з ними проблематично.
Вставка клієнтського коду на льоту:
T: Легко здійснюється за допомогою відповідного API, підтримується створення і подальше виконання клієнтських функцій, одноразове виконання injected-коду, заміна існуючих функцій у вихідній web-сторінці, а також виконання клієнтських функцій в callback-ах node.js-функцій з прив'язкою до тестового контролера.
N: Є функціональність для виконання JS-коду на клієнтській стороні, або навіть вставки цілого script-блоку, але інтеграції з тест-контролером не надається. Підходить для простого синхронного интегрируемого JS-коду, але в більш загальному випадку інтеграція проблематична.
Описи тверджень і очікувань:
T: За замовчуванням — звичайний мову тверджень chai/expect, можна використовувати і будь-яку іншу сумісну бібліотеку тверджень.
N: Розширений мова
assert
та
expect
, що включає засоби для опису стану сторінки, в тому числі наявності елемента і фокуса на ньому, приналежність до CSS-класу і так далі. Виглядає зручно, проте в першу чергу зумовлено відсутністю повноцінної підтримки асинхронних операцій в тесті, при необхідності наявності тверджень і очікувань.
Управління досліджуваної web-сторінкою:
T: Передбачає можливість mock-а для блокують виконання сценаріїв функцій
alert
,
confirm
і так далі, з можливістю завдання значення, що повертається. Підтримуються складні маніпуляції з DOM-моделлю, емуляція інтерфейсу взаємодії з елементами управління, і навіть можливість призупинення JS-сценарію за рахунок обгортання клієнтських JS-функцій
N: Підтримуються команди для безпосереднього управління інструкції вікном браузера і підлягає DOM-моделлю, інтеграція з клієнтським JS-сценарієм утруднена
Робота з курсором миші:
T: Надається віртуальний курсор, за допомогою якого здійснюються hover, click і drag-події для цільовим візуальних елементів сторінки. В процесі виконання тесту можна спостерігати за переміщенням курсору і виконуваними діями.
N: Засоби для роботи з курсором взагалі є — це функції з webdriver api, проте працювати з діями, складніше одиночного лівого кліка, досить проблематично — взяти хоча б подвійне клацання.
Реалізація взаємодії з браузером в TestCafe і NightWatch
Фреймворк NightWatch ґрунтується на відомій, в деякій мірі вже є традиційною, бібліотеці Selenium webdriver, переваги якої включають усталене API, високу ступінь документованості і велике Q&A в інтернеті, а також універсальний і досить низькорівневий доступ до браузера… Взаємодія з браузерами організується за допомогою webdriver-ов, по одному на кожен оглядач.
Основний недолік — webdriver-и існують далеко не для всіх браузерів, а також вимагають окремого оновлення при зміні версії самого браузера, а ще для роботи необхідна наявність зовнішньої залежності від Java. Більш детально про технології webdriver можна прочитати в http://www.w3.org/TR/webdriver/
Фреймворк TestCafe заснований на підході, в якому взаємодія з додатком браузера зведено до мінімуму — використовуються тільки функції відкриття вікна/вкладки, зміни розміру вікна, посилання на URL і деякі інші.
Взаємодія з web-сторінкою проводиться за допомогою web-proxy сервера testcafe-hammerhead, що здійснює завантаження віддаленого web-сторінки і модифікацію вихідного JS-коду таким чином, щоб виконати кроки функціонального тесту, за допомогою інтеграції з DOM-моделлю, пошуку і управління візуальними елементами, виконання довільного JS-коду і так далі. https://github.com/DevExpress/testcafe-hammerhead/)
Метод TestCafe більш універсальний, оскільки потенційно може працювати з будь-яким браузером, що підтримує HTML5 і ES 5+, в той час як NightWatch вимагає відповідний webdriver. Крім того, це дозволяє проганяти тести не тільки на локальному браузері, але на будь-якому браузері в мережі, включаючи будь-які мобільні — без встановлення будь-якого телефону.
Приклад тестування вищерозглянутого web-додатки в браузері на Android показаний в наступному відео: https://youtu.be/2na5jkqvUx0
Однак testcafe-hammerhead має потенційні недоліки: накладні витрати на аналіз і модифікацію вихідного JS-коду досліджуваної сторінки, вироблені в свою чергу JS-коді ядра Testcafe, а також теоретично некоректна робота досліджуваної web-сторінки або інтеграції тесту, якщо вихідний код проксирован невірно. (Наприклад, заміщення alert-вікна в testcafe можна обійти таким прикладом http://pastebin.com/p6gLWA75 — і неумешленно або спеціально «підвісити» його виконання)
Висновки
Звичайно, selenium-webdriver, на якому заснований Nightwatch, є популярним і широко відомим рішенням, що має стандартний API-інтерфейс, що безсумнівно є його перевагою. Крім того, в суміжних областях завдань, наприклад автоматизації цільового web-ресурсу в заданому браузері — фактично написанні бота для віддаленого web-сайту — selenium-webdriver підходить краще.
Однак для функціонального тестування розроблюваного або підтримуваного web-додатки TestCafe безсумнівно попереду з широкого ряду причин:
1) Запуск тестів в будь-якому браузері, в тому числі мобільних телефонах і планшетах, причому це може відбуватися пакетним чином для всіх интересуемых браузерів і не вимагає установки додаткового ПЗ.
2) Зручність написання тесту на ES2016, що включає async/await-конструкції для послідовного запису коду, імпорт програмних елементів з самого проекту, передачу функцій клієнт і назад і так далі — широкі можливості по інтеграції і управління клієнтським web-додатком.
3) Широка підтримка selector-ів для візуальних елементів, легке взаємодія з DOM-моделлю, віртуальний курсор миші, емуляція різноманітних і складних взаємодій користувача зі сторінкою.
Таким чином, серед існуючих open-source рішень для функціонального тестування web-додатків, TestCafe виглядає дуже привабливим варіантом, до того ж при поєднанні легковажності і функціональності.
Автор статті не має відношення до розробки TestCafe і Nightwatch і розглядав ці інструменти як звичайний користувач.
Вихідні коди програми та функціональних тестів: https://github.com/IhostVlad/react-todomvc-with-functional-tests
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.