Меня зовут Виталий Котов, я довольно много занимаюсь автоматизацией тестирования и мне это нравится. Недавно я участвовал в проекте по настройке автоматизации «с нуля» на стеке TypeScript + Protractor + Jasmine. Для меня этот стек был новым и необходимую информацию я искал на просторах интернета.
Самые полезные и толковые мануалы мне удалось найти только на английском языке. Я решил, что на русском тоже надо такой сделать. Расскажу только основы: почему именно такой стек, что надо настроить и как выглядит самый простой тест.
Сразу оговорюсь, что довольно редко работаю с NodeJS, npm и в целом с серверным JavaScript (тем более с TypeScript). Если где-то найдете ошибку в терминологии или какое-то из моих решений можно улучшить — буду рад узнать об этом в комментариях от более опытных ребят.
К слову, у меня уже была подобная статья: «Разворачиваем автоматизацию за пару часов: PHPUnit, Selenium, Composer».
Задача
Прежде всего давайте разберемся с тем, какую задачу мы решаем. У нас есть веб-приложение, написанное с использованием AngularJS. Это JavaScript-фреймворк, на основе которого довольно часто пишутся веб-проекты.
В рамках этой статьи мы не будем рассматривать плюсы и минусы AngularJS-проектов. Лишь пара слов об особенностях таких проектов с точки зрения написания для них e2e-тестов.
Довольно важным аспектом автоматизации тестирования является работа с элементами страницы, которая происходит при помощи локаторов. Локатор — это строка, составленная по определенным правилам и идентифицирующая UI-элемент: один или несколько.
Для веба чаще всего используются CSS и Xpath. Иногда, если на странице есть элемент с уникальным ID, можно искать по нему. Однако мне кажется, что WebDriver все равно этот ID превращает в CSS-локатор в конечном итоге и уже работает с ним.
Если же мы посмотрим на HTML-код какого-нибудь AngularJS-проекта, мы увидим у элементов множество атрибутов, которых нет в классическом HTML:
Код взят со страницы protractor-demo.
Все атрибуты, начинающиеся с ng-* используются AngularJS для работы с UI. Довольно типичной является ситуация, когда у элементов кроме этих управляющих атрибутов нет никаких других, что несколько усложняет процесс составления качественных локаторов.
Кто много занимался автоматизацией, тот знает о ценности таких UI, для которых без труда можно составлять локаторы. Ведь это редкость для крупных проектов. :)
Собственно, вот для такого проекта нам и надо настроить автоматизацию тестирования. Поехали!
Что есть что
Прежде всего давайте разберемся, для чего нужна каждая составляющая нашего стека.
Protractor — это тестовый фреймворк на основе WebDriverJS. Именно он будет запускать наши браузеры, заставлять их открывать нужные страницы и взаимодействовать с необходимыми элементами.
Этот фреймворк специально заточен под AngularJS-проекты. Он предоставляет дополнительные способы задавать локаторы:
element(by.model('first'));
element(by.binding('latest'));
element(by.repeater('some'));
Полный список можно посмотреть на странице мануала.
Эти методы упрощают создание и поддержку локаторов на проекте. Однако, надо понимать, что «под капотом» все это в любом случае преобразуется в css. Дело в том, что протокол W3C, на основе которого происходит взаимодействие в WebDriver, умеет работать только с конечным набором локаторов. Этот список можно посмотреть на сайте w3.org.
TypeScript — это язык программирования, созданный компанией Microsoft. TypeScript отличается от JavaScript возможностью типизировать переменные, поддержкой использования полноценных классов и возможностью подключения модулей.
Написанный на TS-код для работы с движком V8 транспилируется в JS-код, который уже исполняется. В ходе этого превращения происходит проверка кода на его соответствие. Например, он не “скомпилируется”, если вместо int в функцию где-то явно передается string.
Jasmine — фреймворк для тестирования JavaScript-кода. По сути именно благодаря нему наш JS-код превращается в то, что мы привыкли называть тестом. Он же управляет этими тестами.
Чуть ниже мы посмотрим на его возможности.
Сборка и настройка проекта
Что ж, с набором фреймворков мы определились, давайте теперь все это дело соберем.
Для работы с кодом я выбрал Visual Studio Code от Microsoft. Хотя многие пишут в WebStorm или даже Intellij Idea от JetBrains.
У меня уже были установлены NodeJS (v11.6.0) и NPM (6.9.0). Если у вас нет, это не проблема, установить их не составит труда. Все довольно подробно описано на официальном сайте.
Вместо NPM можно использовать Yarn, хотя для небольшого проекта это не важно.
В нашем IDE мы создаем новый проект. В корне проекта создаем package.json — именно в нем мы опишем все те пакеты, которые нам необходимы для проекта.
Можно его создать с помощью команды npm init. А можно просто скопировать содержимое в файл.
Изначально package.json выглядит так:
{
"name": "protractor",
"dependencies": {
"@types/node": "^10.5.2",
"@types/jasmine": "^3.3.12",
"protractor": "^5.4.2",
"typescript": "^3.4.1"
}
}
После мы выполняем команду npm install для установки всех необходимых модулей и их зависимостей (ну вы помните ту самую картинку про то, что тяжелее черной дыры...)
В результате у нас должна появиться директория node_modules. Если она появилась, то все идет по плану. Если нет — стоит заглянуть в результат выполнения команды, там обычно все довольно подробно описано.
TypeScript и его конфиг
Для установки TypeScript нам понадобится npm:
npm install -g typescript
Убедимся, что он установился:
$ tsc -v
Version 3.4.1
Похоже, все в порядке.
Теперь надо создать конфиг для работы с TS. Он также должен лежать в корне проекта и называться tsconfig.json
Его содержимое будет таким:
{
"compilerOptions": {
"lib": ["es6"],
"strict": true,
"outDir" : "output_js",
"types" : ["jasmine", "node"]
},
"exclude": [
"node_modules/*"
]
}
Если коротко, мы в этом конфиге указали следующее:
- В какую директорию класть итоговый JS-код (в нашем случае это output_js)
- Включили strict mode
- Указали с какими фреймворками работаем
- Исключили node_modules из компиляции
У TS существует огромное множество настроек. Для нашего проекта этих пока хватит. Более подробно можно изучить на сайте typescriptlang.org.
Теперь давайте посмотрим, как работает команда tsc, которая будет превращать наш TS-код в JS-код. Для этого создадим простой файл check_tsc.ts со следующим содержимым:
saySomething("Hello, world!");
function saySomething(message: string) {
console.log(message);
}
И далее выполним команду tsc (для этого надо быть внутри директории с проектом).
Мы увидим, что у нас появилась директория output_js и внутри появился аналогичный js-файл вот с таким содержимым:
"use strict";
saySomething("Hello, world!");
function saySomething(message) {
console.log(message);
}
Этот файл уже можно запускать с помощью команды node:
$ node output_js/check_tsc.js
Hello, world!
Итак, мы написали нашу первую программу на TypeScipt, мои поздравления. Давайте теперь писать тесты. :)
Protractor конфиг
Для Protractor нам тоже понадобится конфиг. Но он уже будет не в виде json, а в виде ts-файла. Назовем его config.ts и напишем там следующий код:
import { Config } from "protractor";
export const config: Config = {
seleniumAddress: "http://127.0.0.1:4444/wd/hub",
SELENIUM_PROMISE_MANAGER: false,
capabilities: {
browserName: "chrome",
/*chromeOptions: {
args: [ "--headless", "--window-size=800,600" ]
}*/
},
specs: [
"Tests/*Test.js",
]
};
В этом файле мы указали следующее:
Во-первых — путь до запущенного Selenium-сервера. Его довольно просто запустить, надо скачать jar-файл Standalone Server и необходимые драйверы (например, chrome-драйвер для браузера Chrome). Далее прописать следующую команду:
java -jar -Dwebdriver.chrome.driver=/path/to/chromedriver /path/to/selenium-server-standalone.jar
В результате мы должны увидеть такой вывод:
23:52:41.691 INFO [GridLauncherV3.launch] - Selenium build info: version: '3.11.0', revision: 'e59cfb3'
23:52:41.693 INFO [GridLauncherV3$1.launch] - Launching a standalone Selenium Server on port 4444
2019-05-02 23:52:41.860:INFO::main: Logging initialized @555ms to org.seleniumhq.jetty9.util.log.StdErrLog
23:52:42.149 INFO [SeleniumServer.boot] - Welcome to Selenium for Workgroups....
23:52:42.149 INFO [SeleniumServer.boot] - Selenium Server is up and running on port 4444
Порт 4444 является дефолтным. Его можно задать с помощью параметра -port или через конфиг параметром «seleniumArgs» => "-port".
Если хочется проще и быстрее: можно скачать npm-пакет webdriver-manager.
И далее управлять сервером при помощи команд start, shutdown и так далее. Особой разницы нет, просто мне привычнее работать с jar-файлом. :)
Во-вторых — мы указали, что не хотим использовать Promise-менеджер. Об этом чуть позже.
В-третьих — мы указали capabilities для нашего браузера. Я закомментировал часть, например то, что мы можем довольно легко запускать браузер в headless-режиме. Это классная фича, но она не позволит визуально наблюдать за нашими тестами. А пока мы только учимся — хотелось бы. :)
В-четвертых — мы указали маску для спеков (тестов). Все, что лежит в папке Tests и оканчивается на Test.js. Почему именно на js, а не на ts? Все потому, что в итоге Node будет работать именно c JS-файлами, а не с TS-файлами. В этом важно не запутаться, особенно в начале работы.
Теперь создаем папку Tests и пишем первый тест. Он будет делать следующее:
- Отключит проверку на то, что это Angular-страница. Без этого мы получим вот такое сообщение об ошибке: Error while running testForAngular. Само собой, для Angular-страниц эту проверку выключать не надо.
- Зайдет на страницу Google.
- Проверит, что есть поле ввода текста.
- Введет текст “protractor”.
- Кликнет по кнопке submit (у нее довольно сложный локатор, так как кнопок две и первая невидимая).
- Дождется, что URL будет содержать слово “protractor” — это значит, что мы все сделали верно и поиск начался.
Вот такой код у меня получился:
import { browser, by, element, protractor } from "protractor";
describe('Search', () => {
it('Open google and find a text', async () => {
// Создаем объект для работы с ожиданиями
let EC = protractor.ExpectedConditions;
// выключаем проверку на AngularJS
await browser.waitForAngularEnabled(false);
// открываем страницу Google
await browser.get('https://www.google.com/');
// создаем элемент по css = input[role='combobox']
let input_button = element(by.css("input[role='combobox']"));
// ждем появление этого элемента (события presenceOf)
await browser.wait(EC.presenceOf(input_button), 5000);
// пишем в элемент текст “protractor”
await input_button.sendKeys("protractor");
// создаем элемент кнопки сабмита по css
let submit_button = element(by.css(".FPdoLc input[type='submit'][name='btnK']"));
// дожидаемся его появления на странице (не обязательно, ведь мы уже дождались input-элемента, значит страница загрузилась)
await browser.wait(EC.presenceOf(submit_button), 5000);
// кликаем по кнопке сабмита
await submit_button.click();
// ждем, когда URL будет содержать текст 'protractor'
await browser.wait(EC.urlContains('protractor'), 5000);
});
});
В коде мы видим, что все начинается с функции describe(). Она пришла к нам из фреймворка Jasmine. Это обертка для нашего сценария. Внутри нее могут быть функции beforeAll() и beforeEach() для выполнения каких-либо манипуляций перед всеми тестами и перед каждым тестом. Сколько угодно функций it() — это, собственно, наши тесты. В конце, если определены, будут выполнены afterAll() и afterEach() для манипуляций после каждого теста и всех тестов.
Обо всех возможностях Jasmine не буду рассказывать, о них можно почитать на сайте jasmine.github.io
Для запуска нашего теста сначала надо превратить TS-код в JS-код, а затем запустить его:
$ tsc
$ protractor output_js/config.js
Наш тест запустился — мы молодцы. :)
Если тест не запустился, стоит проверить:
- Что код написан верно. В целом, если в коде есть критические ошибки, мы их выловим в процессе выполнения команды tsc.
- Что Selenium Server запущен. Для этого можно открыть URL http://127.0.0.1:4444/wd/hub — там должен быть интерфейс Selenium-сессий.
- Что Chrome запускается нормально со скачанной версией chrome-driver. Для этого на странице wd/hub/ надо кликнуть Create Session и выбрать Chrome. Если он не запустится, значит надо либо обновить Chrome, либо скачать другую версию chrome-driver.
- Если все это не помогло, можно проверить, точно ли успешно завершилась команда npm install.
- Если все написано правильно, но все равно ничего не запускается — попробуйте погуглить ошибку. Это чаще всего помогает. :)
NPM scripts
Для того, чтобы сделать жизнь проще, можно часть команд сделать npm-алиасами. Например, я бы хотел перед каждым запуском тестов сначала удалять директорию с предыдущими JS-файлами и пересоздавать ее с новыми.
Для этого в package.json добавим пункт “scripts”:
{
"name": "protractor",
"scripts": {
"test": "rm -rf output_js/; tsc; protractor output_js/config.js"
},
"dependencies": {
"@types/node": "^10.5.2",
"@types/jasmine": "^3.3.12",
"protractor": "^5.4.2",
"typescript": "^3.4.1"
}
}
Теперь введя команду npm test произойдет следующее: удалится директория output_js со старым кодом, создастся заново и в нее запишется новый JS-код. После чего сразу произойдет запуск тестов.
Вместо этого набора команд можно указать любой другой, который нужен для работы лично вам. Например, вы можете запускать и гасить selenium-сервер между запусками тестов. Хотя это, конечно, проще контролировать внутри самого кода тестов.
Немного о Promise
В конце немного расскажу о Promise, async / await и о том, чем написание тестов на NodeJS отличается от той же Java или Python.
JavaScript является асинхронным языком. Это означает, что код не всегда выполняется в том порядке, в котором он написан. В том числе это касается HTTP-запросов, а мы помним, что код общается с Selenium Server именно по HTTP.
Promise (обычно так и называют «промисы») предоставляют удобный способ организации асинхронного кода. О них можно довольно подробно почитать на learn.javascript.ru.
По сути это объекты, позволяющие один код сделать зависимым от выполнения другого, тем самым гарантируя определенный порядок. Protractor очень активно работает с этими объектами.
Давайте рассмотрим на примере. Предположим, что мы выполняем такой код:
driver.findElement().getText();
В Java мы ожидаем, что нам вернется объект типа String. В Protractor все не совсем так, нам вернется объект Promise. И просто так распечатать его с целью дебага не выйдет.
Обычно нам не надо распечатывать полученное значение. Нам надо его передать в какой-то другой метод, который уже будет с этим значением работать. Например, проверит текст на соответствие ожидаемому.
Подобные методы в Protractor на вход принимают тоже Promise-объекты, так что проблем нет. Но, если все же хочется увидеть значение, нам пригодится функция then().
Вот так мы можем распечатать текст кнопки на странице Google (обратите внимание, что т.к. это button, текст находится внутри атрибута value):
// создаем элемент
let submit_button = element(by.css(".FPdoLc input[type='submit'][name='btnK']"));
// дожидаемся его появления
await browser.wait(EC.presenceOf(submit_button), 5000);
// при помощи then() получаем текст
await submit_button.getAttribute("value").then((text) => {
console.log(text);
});
Что же касается ключевых слов async / await — это чуть более новый подход к работе с асинхронным кодом. Он позволяет избегать promise hell, который раньше образовывался в коде из-за большого количества вложенностей. Тем не менее, полностью от Promise избавиться не получится и уметь работать с ними надо. Об это понятно и подробно можно почитать в статье Конструкция async/await в JavaScript: сильные стороны, подводные камни и особенности использования.
Домашнее задание
В качестве домашнего задания предлагаю написать тесты для страницы, написанной на AngularJS: protractor-demo.
Не забудьте убрать из кода строчку про выключение проверки страницы на AngularJS. И обязательно поработайте с локаторами, специально созданными для AngularJS. В этом нет особой магии, но это довольно удобно.
Итог
Давайте подводить итоги. Нам удалось написать тесты, которые работают на связке TypeScript + Protractor + Jasmine. Мы научились собирать такой проект, создавать необходимые конфиги и написали первый тест.
По ходу дела немного обсудили особенности работы с автотестами на JavaScript. Вроде неплохо для пары часов. :)
Что почитать, куда посмотреть
У Protractor есть довольно хороший мануал с примерами на JavaScript: https://www.protractortest.org/#/tutorial
У Jasmine есть мануал: https://jasmine.github.io/pages/docs_home.html
У TypeScipt есть хороший get started: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html
На Медиуме есть неплохая статья на английском про TypeScript + Protractor + Cucumber: https://medium.com/@igniteram/e2e-testing-with-protractor-cucumber-using-typescript-564575814e4a
А в своем репозитории я выложил итоговый код того, что мы с вами обсуждали в этой статье: https://github.com/KotovVitaliy/HarbProtractorJasmineJasmine.
В интернете можно найти примеры более сложных и больших проектов на этом стеке.
Спасибо за внимание! :)
Комментарии (9)
L2jLiga
13.05.2019 14:58В protactor ^5 под капотом используются Jasmine 2.x и jasminewd2(адаптер jasmine 2 -> webdriver) и правильно было бы в зависимостях использовать тайпинги для второго Jasmine и тайпинги для адаптера. Так что я бы поставил бы
@types/jasmine@^2
и@types/jasminewd2
.
А вот в Protractor 6 уже используется третий Jasmine без всяких "сторонних" адаптеров.
mkovalevskyi
13.05.2019 22:01Спасибо за упоминание
"...WebDriver, умеет работать только с конечным набором локаторов. Этот список можно посмотреть на сайте w3.org...."
, а то давно пользуюсь но никогда не доходили руки посмотреть именно спецификации.
Но в связи с вышеописанным возник вопрос — так кто кому следует, собственно?
Ибо по ссылке можно прочитать«This specification is derived from the popular Selenium WebDriver browser automation framework.»
— тоесть этот стандарт написан по мотивам селениума. Но и стандарт, и селениум — они как-бы не статичны, да и конторы их пилят немного разные…Cerberuser
14.05.2019 04:22По формулировке похоже, что Selenium сгенерировал начальный вариант, а W3C его с некоторыми модификациями принял как стандарт, которому теперь следует в том числе и сам Selenium.
pettit3301
16.05.2019 11:56Вместо этого набора команд можно указать любой другой, который нужен для работы лично вам. Например, вы можете запускать и гасить selenium-сервер между запусками тестов. Хотя это, конечно, проще контролировать внутри самого кода тестов.
Расскажите пожалуйста чуть подробнее — в каких случая необходимо запускать и гасить selenium-сервер между запусками тестов?nizkopal Автор
16.05.2019 11:57Привет. Имелось в виду между запусками не в рамках одного набора, а между глобальными запусками.
Например, если вы запускаете пачку тестов два раза в день, между этими событиями нет смысла держать selenium-сервер запущенным.pettit3301
16.05.2019 15:27Аа понял, спасибо! А у вас случайно нет линки на проект с хорошим примером где такое используется или статью?
Drag13
В который раз пишу — ну не ставьте вы tsc глобально, забудьте как страшный сон. TSC это то, от чего зависит ваш проект. Он должен стоять локально. Это даст вам возможность:
Если все таки надо что-то глобальное (например angular/cli или create-react-app) лучше воспользоваться npx — подробнее тут.
nizkopal Автор
Да, это разумно, спасибо за замечание и ссылку.