Protractor

Protractor — движок для запуска системных (end-to-end, браузерных) тестов. Внутри он использует seleniumс драйверами для браузера (chromedriver), а сами тесты пишутся с синтаксисом jasmine. Про него и карму я уже писал, впрочем mocha и cucumber тоже поддерживаются.

Из особенностей — protractor имеет интеграцию с angular (находит модели и repeat-директивы) — отсюда и название слов (angle — угол, protractor — транспортир, т.е. угловая линейка), но может тестировать и любые другие сайты. Из недостатков —

  1. Мне всё-таки пришлось добавить angular.js и ng-app к body на не-ангулярную страницу логина что-бы работал protractor
  2. В отличие от selenium, тесты надо писать вручную, никакого browser-плагина нет для записи кликов как то было в selenium ide

Ставим, запускаем selenium и в другом процессе запускаем исполнение тестов по заданной конфигурации:

npm install protractor -g
webdriver-manager start
clear && protractor conf.js --verbose

 

Конфигурация в стиле node-модуля, просто экспортирует данные о том где запущен selenium, какие spec-файлы надо запустить, какие переменные сделать глобальными.

exports.config = {
    seleniumAddress: 'http://localhost:4444/wd/hub',
    specs: [
        'login.js',
        'profile.js',
        'categories.js',
        'products.js'
    ],
    baseUrl: 'http://kurapov.name',
    onPrepare: function() {
        global.$p = protractor.getInstance();
        //global.$ = require('./myjquery.js').$;
        browser.driver.manage().window().setSize(1200, 800);
    },
    capabilities: [
        {
            'browserName': 'chrome',
            'chrome.cli.args': ['--ignore-ssl-errors=true', '--web-security=false']
        },
        {
            'browserName': 'phantomjs',
            'phantomjs.cli.args': ['--debug=false', '--ignore-ssl-errors=true', '--web-security=false', '--webdriver-loglevel=INFO']
        }
    ]
    jasmineNodeOpts: {
        showColors: true
    }

Недостатки работы с DOM

В моём случае я использую свою wrapper-функцию $() для обращения к DOM. Проблема в том, что protractor общается с selenium драйверами через свои интерфейсы, т.е. вы не можете напрямую как-то обращаться к DOM браузера или с удобством вызывать javascript. 

Локаторы

  • by.css
  • by.model 
  • by.repeater 
  • by.id
  • by.js
  • by.className 
  • by.linkText
  • by.name
  • by.partialLinkText
  • by.tagName
  • by.xpath
  • by.addLocator
  • by.binding
  • by.buttonText
  • by.cssContainingText
  • by.exactBinding
  • by.options
  • by.partialButtonText

Найти элементы можно с помощью локаторов, из которых самый популярный — by.css(‘#myelement’). Дальше надо обернуть локатор выборкой элемента:

element(by.css('#myelement')).click();

Проблема в том, что элементов может быть много, а может и вообще не быть. Поэтому ваш тест должен знать что ищет

element.all(by.css('.row')).first()

Это плохо, потому что если вы обращаетесь к элементу которого ещё нет, то protractor выкинет ошибку и stacktrace. Это усложняет написание повторяемых тестов.

Поэтому для себя я написал обёртку вида $(‘#myelement .row’,3).click() которая будет выбирать третий элемент.

Вторая проблема с локаторами — асинхронность. Т.е. это конечно хорошо что гибко, но тесты получаются сложными. Методы действия над элементами как .click(), .getText() или .count() возвращают promise-обьект и получается что для правильной последовательности теста, надо городить лестницы из click-then

Та же проблема была и с селениумом кстати. Это неопределённость от синхронизации приложения с DOM, из-за которой приходится писать различные хаки с таймаутом

Практически полезные фишки

В файле тестов полезно перед каждым тестов вставлять ожидание..

describe('profile', function () {
    beforeEach(function () {
        $p.sleep(400); //в миллисекундах
        $p.ignoreSynchronization = true; //для страниц без angular синхронизации
        console.log(jasmine.getEnv().currentSpec.description + ' started:');
    });
    ..
});

При работе со списками, в зависимости от того используете вы angular или нет, имеет смысл обращаться либо by.repeater, либо по css, например:

#countries select option:nth-child(2) 
Тестирование субмита форм с помощью энтера.. да и вообще эмуляция нажатий клавиш тоже возможна..
element( by.model('name') ).sendKeys("James Bond", protractor.Key.ENTER); 
Protractor умеет работать с алертами и confirm-диалогами..
$p.switchTo().alert().accept();
Он и файлы может загружать, но для этого надо заинклудить path из nodejs
var path = require('path');
$("#new_avatar input[type='file']").val(path.resolve(__dirname,'./testfiles/artjom_kurapov.jpg'));

В приведённом мною выше конфиге вы увидите, что я пытаюсь несколько браузеров использовать. Увы, phantomjs у меня пока не удалось успешно запустить. Он поднимается, открывает страницу, ищет элемент и потом падает

«errorMessage»:»Element is not currently interactable and may not be manipulated»

Дебаг

В коде теста, браузер можно остановить. Для меня это была основной способ разобраться что не так
browser.pause();
Кроме того protractor можно запустить в дебаг режиме, тогда клавишами c и d можно проматывать комманды теста
protractor debug conf.js
Есть ещё вариант открыть страницу и попробовать интерактивный синтаксис обращения к DOM. Об этом подробней в видео советую посмотреть — это научит вас лексикону
node ./bin/elementexplorer.js http://kurapov.name

Организация тестов

Тесты можно сгруппировать в suite, для этого в конфиге можно задать:
  suites: {
    users: 'spec/*.user.js',
    admins: 'spec/*.admin.js'
  } 
И запускать только конкретную группу:
protractor conf.js --suite admins
Когда приложение слишком большое, то имеет смысл создавать т.н. page objects — отдельные обьекты к которым будут привязываться элементы по css, а в самих тестах уже использовать только свойства (property) этих обьектов.

Написание тестов достаточно противоречиво — с одной стороны они должны быть изолированы что-бы можно было запустить каждый тест самим по себе, с другой — короткими, без повторения нудных операций авторизации и подхода к нужному месту. Для этого я тесты обёртываю в такую конструкцию..

myApp.usePageAs('[email protected]', 'admin/dash', function () {
//test here
});
Код myApp позволяет мне логиниться если я ещё не залогинен, используя стандартные аккаунты для тестов, открывать страницу и запускать функцию теста. В myApp я помещаю и методы проверки каких-то оповещений (messages/notifications), на случай если DOM и стили оных поменяются.