Upload
others
View
43
Download
0
Embed Size (px)
Citation preview
Гейзенбаг, 2017-06-04 Илья Коробицын
Пишем Selenium тесты на JS для тестирования Angular: плюсы,
минусы, подводные камни
2
О докладчике Илья Коробицын
mailto: [email protected] Telegram: @korobochka
Работаю в Grid Dynamics
Занимаемся автоматизациейтестирования
Бэкенд на Java Фронтенд на JavaScript
3
План доклада Какие сложности приходится решать при написании Selenium
тестов для современных веб приложений
Что такое Protractor и как он помогает с тестированием Angular приложений
С какими неожиданностями при этом придётся столкнуться
Чему можно научиться из всего этого
4
Подопытное приложение
5
Подопытное приложение
6
Подопытное приложение
7
Немного про Angular
8
Немного про Angular
<tr ng-repeat="result in results"> <td> {{result.timestamp | date:'mediumTime'}} </td> <td>{{result.nick}}</td> <td>{{result.age}}</td> <td>{{result.result}}</td></tr>
9
Немного про Angular
.......
<tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:21 PM </td> <td class="ng-binding">CoolNick</td> <td class="ng-binding">15</td> <td class="ng-binding">CoolNick2002</td></tr><tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:04 PM </td> <td class="ng-binding">Nick</td> <td class="ng-binding">25</td> <td class="ng-binding">Nick1992</td></tr>.......
.......
<tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:21 PM </td> <td class="ng-binding">CoolNick</td> <td class="ng-binding">15</td> <td class="ng-binding">CoolNick2002</td></tr><tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:04 PM </td> <td class="ng-binding">Nick</td> <td class="ng-binding">25</td> <td class="ng-binding">Nick1992</td></tr>.......
10
Немного про Angular
.......
<tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:21 PM </td> <td class="ng-binding">CoolNick</td> <td class="ng-binding">15</td> <td class="ng-binding">CoolNick2002</td></tr><tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:04 PM </td> <td class="ng-binding">Nick</td> <td class="ng-binding">25</td> <td class="ng-binding">Nick1992</td></tr>.......
.......
<tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:21 PM </td> <td class="ng-binding">CoolNick</td> <td class="ng-binding">15</td> <td class="ng-binding">CoolNick2002</td></tr><tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:04 PM </td> <td class="ng-binding">Nick</td> <td class="ng-binding">25</td> <td class="ng-binding">Nick1992</td></tr>.......
11
Немного про Angular
.......
<tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:21 PM </td> <td class="ng-binding">CoolNick</td> <td class="ng-binding">15</td> <td class="ng-binding">CoolNick2002</td></tr><tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:04 PM </td> <td class="ng-binding">Nick</td> <td class="ng-binding">25</td> <td class="ng-binding">Nick1992</td></tr>.......
.......
<tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:21 PM </td> <td class="ng-binding">CoolNick</td> <td class="ng-binding">15</td> <td class="ng-binding">CoolNick2002</td></tr><tr ng-repeat="result in results" class="ng-scope"> <td class="ng-binding"> 5:36:04 PM </td> <td class="ng-binding">Nick</td> <td class="ng-binding">25</td> <td class="ng-binding">Nick1992</td></tr>.......
12
Про сложности Элементами на странице управляет Angular, а не программисты
Те не ставят id, так как они им не нужны
Все элементы получаются безликие, связь с данными неявна
Тяжело писать селекторы
13
Подопытное приложение
14
Про сложности Обновлением элементов на странице также занимается
фреймворк
Происходит это после определённых триггеров Завершение HTTP запроса Таймер
На каждое обновление приходится писать отдельный waiter
15
Посмотрим на тесты
16
Пример на Java
@Testpublic void testResultAddedToHistory() throws Exception {
final List<WebElement> inputs = driver.findElements(By.tagName("input"));
inputs.get(0).sendKeys("Korobochka");inputs.get(1).sendKeys("17");driver.findElement(By.className("btn")).click();new WebDriverWait(driver, 10).until(webDriver ->
!webDriver.findElement(By.tagName("h3")).getText() .contains("Loading"));
WebElement targetElement = driver.findElement( By.xpath("//tbody/tr[1]/td[last()]"));
assertEquals(targetElement.getText(), "Korobochka2000");}
17
Пример на Java
@Testpublic void testResultAddedToHistory() throws Exception {
final List<WebElement> inputs = driver.findElements(By.tagName("input"));
inputs.get(0).sendKeys("Korobochka");inputs.get(1).sendKeys("17");driver.findElement(By.className("btn")).click();new WebDriverWait(driver, 10).until(webDriver ->
!webDriver.findElement(By.tagName("h3")).getText() .contains("Loading"));
WebElement targetElement = driver.findElement( By.xpath("//tbody/tr[1]/td[last()]"));
assertEquals(targetElement.getText(), "Korobochka2000");}
18
Пример на Java
@Testpublic void testResultAddedToHistory() throws Exception {
final List<WebElement> inputs = driver.findElements(By.tagName("input"));
inputs.get(0).sendKeys("Korobochka");inputs.get(1).sendKeys("17");driver.findElement(By.className("btn")).click();new WebDriverWait(driver, 10).until(webDriver ->
!webDriver.findElement(By.tagName("h3")).getText() .contains("Loading"));
WebElement targetElement = driver.findElement( By.xpath("//tbody/tr[1]/td[last()]"));
assertEquals(targetElement.getText(), "Korobochka2000");}
19
Protractor
20
Пример с Protractor
it('should add latest result to history', function() {
element(by.model('nick')).sendKeys('Korobochka'); element(by.model('age')).sendKeys('17'); element(by.buttonText('Submit')).click();
let fistRow = element.all(by.repeater('result in memory')).get(0); let firstRowResult = fistRow.element(by.binding('result.result')); expect(firstRowResult.getText()).toEqual('Korobochka2000');});
21
Пример с Protractor
it('should add latest result to history', function() {
element(by.model('nick')).sendKeys('Korobochka'); element(by.model('age')).sendKeys('17'); element(by.buttonText('Submit')).click();
let fistRow = element.all(by.repeater('result in memory')).get(0); let firstRowResult = fistRow.element(by.binding('result.result')); expect(firstRowResult.getText()).toEqual('Korobochka2000');});
22
Пример с Protractor
it('should add latest result to history', function() {
element(by.model('nick')).sendKeys('Korobochka'); element(by.model('age')).sendKeys('17'); element(by.buttonText('Submit')).click();
let fistRow = element.all(by.repeater('result in memory')).get(0); let firstRowResult = fistRow.element(by.binding('result.result')); expect(firstRowResult.getText()).toEqual('Korobochka2000');});
23
Angular 2+
StartedF
Failures:1) Protractor Demo App should generate nickname correctly Message: Failed: unknown error: angular is not defined (Session info: chrome=58.0.3029.81) (Driver info: chromedriver=2.27.440175 ….)
24
Angular 2+
25
Angular 2+
/** @deprecated use findProviders */findBindings(using: any, provider: string, exactMatch: boolean): any[] { // TODO(juliemr): implement. return [];}
findProviders(using: any, provider: string, exactMatch: boolean): any[] { // TODO(juliemr): implement. return [];}
26
Итого Если у вас Angular 1, то Protractor поможет с селекторами
Если Angular 2+, то увы Пинайте разработчиков добавлять id или уникальные классы в
разметку
Вам почти никогда не придётся писать ожидания
27
Паззлеры
28
Паззлеры Будем тестировать наше подопытное приложение с помощью
Protractor
Я покажу тест и несколько варинтов развития событий (пройдёт / не пройдёт / etc )
Зал голосует
Смотрим результаты запуска теста
Кто-то кратко рассказывает, почему так произошло
29
Паззлер №1
30
Паззлер №1
it('should display consistent info in list', function() { doInputAndClick('Nick', '17');
let listItem = element.all(by.className('results-item')).get(0); let nick = listItem.all(by.tagName('td')).get(1).getText(); let age = listItem.all(by.tagName('td')).get(2).getText();
let actualNickname = listItem.element(by.className('results-result')).getText();
expect(actualNickname).toEqual(nick + (2017 - age));});
31
Паззлер №1
it('should display consistent info in list', function() { doInputAndClick('Nick', '17');
let listItem = element.all(by.className('results-item')).get(0); let nick = listItem.all(by.tagName('td')).get(1).getText(); let age = listItem.all(by.tagName('td')).get(2).getText();
let actualNickname = listItem.element(by.className('results-result')).getText();
expect(actualNickname).toEqual(nick + (2017 - age));});
Вариант 1: Тест успешно проходит
32
Паззлер №1
it('should display consistent info in list', function() { doInputAndClick('Nick', '17');
let listItem = element.all(by.className('results-item')).get(0); let nick = listItem.all(by.tagName('td')).get(1).getText(); let age = listItem.all(by.tagName('td')).get(2).getText();
let actualNickname = listItem.element(by.className('results-result')).getText();
expect(actualNickname).toEqual(nick + (2017 - age));});
Вариант 2: Undefined is not a function
33
Паззлер №1
it('should display consistent info in list', function() { doInputAndClick('Nick', '17');
let listItem = element.all(by.className('results-item')).get(0); let nick = listItem.all(by.tagName('td')).get(1).getText(); let age = listItem.all(by.tagName('td')).get(2).getText();
let actualNickname = listItem.element(by.className('results-result')).getText();
expect(actualNickname).toEqual(nick + (2017 - age));});
Вариант 3: Fail: Expected 'Nick2000' to equal 'NickNaN'
34
Паззлер №1
it('should display consistent info in list', function() { doInputAndClick('Nick', '17');
let listItem = element.all(by.className('results-item')).get(0); let nick = listItem.all(by.tagName('td')).get(1).getText(); let age = listItem.all(by.tagName('td')).get(2).getText();
let actualNickname = listItem.element(by.className('results-result')).getText();
expect(actualNickname).toEqual(nick + (2017 - age));});
Вариант 4: Fail: Expected 'Nick2000' to equal '[object Object]NaN'
35
Паззлер №1it('should display consistent info in list', function() { doInputAndClick('Nick', '17');
let listItem = element.all(by.className('results-item')).get(0); let nick = listItem.all(by.tagName('td')).get(1).getText(); let age = listItem.all(by.tagName('td')).get(2).getText();
let actualNickname = listItem.element(by.className('results-result')).getText();
expect(actualNickname).toEqual(nick + (2017 - age));});
Вариант 1: Тест успешно проходитВариант 2: Undefined is not a functionВариант 3: Fail: Expected 'Nick2000' to equal 'NickNaN'Вариант 4: Fail: Expected 'Nick2000' to equal '[object Object]NaN'
36
Паззлер №1: Запускаем....
Failures:1) Protractor Demo App should display consistent info in list Message: Expected 'Nick2000' to equal '[object Object]NaN'. Stack: Error: Failed expectation
37
Асинхронность Одна из основных особенностей JavaScript
Истоки языка – обработка событий в браузере, «реактивность»
Нельзя совершать блокирующие действия
Значительно усложняет написание простых вещей
38
Асинхронность
39
Что же такое Promise<string> ? «Обещание» дать результат, в данном случае строчку
Результат будет получен асинхронно, прямо сейчас он недоступен
Вытащить значение можно передав в метод `.then` коллбэк, которому дадут результат по готовности
Аналог в Java: CompletableFuture<String>
40
А почему работало? WebdriverJS скрывает от нас часть промисов с помощью Control
Flow
41
А почему работало? WebdriverJS скрывает от нас часть промисов с помощью Control
Flow
Protractor добавляет поддержку промисов к проверкам (expect воспользуется Control Flow, если получит на вход промис)
42
Паззлер №1: Чиним
let nick= listItem.all(by.tagName('td')).get(1).getText();let age = listItem.all(by.tagName('td')).get(2).getText();
let actualNickname = listItem .element(by.className('results-result')).getText();
Promise.all([nick, age]).then(function([nick, age]) { expect(actualNickname).toEqual(nick + (2017 - age));});
43
Паззлер №1: Чиним
(async function() { doInputAndClick('Nick', '17'); let listItem = element.all(by.className('results-item')).get(0); let nick = listItem.all(by.tagName('td')).get(1).getText(); let age = listItem.all(by.tagName('td')).get(2).getText(); let actualNickname = listItem.element(by.className('results-result')).getText(); expect(actualNickname) .toEqual(await nick + (2017 - await age));})();
44
Паззлер №2
45
Паззлер №2
function doInputAndClick(nick, age) { element(by.css('.input-nick')).sendKeys(nick); console.log('INSERTED NICK'); element(by.id('MISTAKE')).click(); element(by.css('.input-age')).sendKeys(age); console.log('INSERTED AGE'); element(by.css('.input-submit')).click(); console.log('CLICKED THE BUTTON');}
46
Паззлер №2
function doInputAndClick(nick, age) { element(by.css('.input-nick')).sendKeys(nick); console.log('INSERTED NICK'); element(by.id('MISTAKE')).click(); element(by.css('.input-age')).sendKeys(age); console.log('INSERTED AGE'); element(by.css('.input-submit')).click(); console.log('CLICKED THE BUTTON');}
Вариант 1: Ненайденные элементы игнорируются
47
Паззлер №2
function doInputAndClick(nick, age) { element(by.css('.input-nick')).sendKeys(nick); console.log('INSERTED NICK'); element(by.id('MISTAKE')).click(); element(by.css('.input-age')).sendKeys(age); console.log('INSERTED AGE'); element(by.css('.input-submit')).click(); console.log('CLICKED THE BUTTON');}
Вариант 2: Вылетит исключение, увидим только первое сообщение в логе
48
Паззлер №2
function doInputAndClick(nick, age) { element(by.css('.input-nick')).sendKeys(nick); console.log('INSERTED NICK'); element(by.id('MISTAKE')).click(); element(by.css('.input-age')).sendKeys(age); console.log('INSERTED AGE'); element(by.css('.input-submit')).click(); console.log('CLICKED THE BUTTON');}
Вариант 3: Вылетит исключение, но всё равно увидим все сообщения в логе
49
Паззлер №2
function doInputAndClick(nick, age) { element(by.css('.input-nick')).sendKeys(nick); console.log('INSERTED NICK'); element(by.id('MISTAKE')).click(); element(by.css('.input-age')).sendKeys(age); console.log('INSERTED AGE'); element(by.css('.input-submit')).click(); console.log('CLICKED THE BUTTON');}
Вариант 4: Какой лог, мы только в Allure репорт писать можем!
50
Паззлер №2function doInputAndClick(nick, age) { element(by.css('.input-nick')).sendKeys(nick); console.log('INSERTED NICK'); element(by.id('MISTAKE')).click(); element(by.css('.input-age')).sendKeys(age); console.log('INSERTED AGE'); element(by.css('.input-submit')).click(); console.log('CLICKED THE BUTTON');}
Вариант 1: Ненайденные элементы игнорируютсяВариант 2: Вылетит исключение, увидим только первое сообщение в логеВариант 3: Вылетит исключение, но всё равно увидим все сообщения в логеВариант 4: Какой лог, мы только в Allure репорт писать можем!
51
Паззлер №2: Запускаем....
INSERTED NICKINSERTED AGECLICKED THE BUTTONFail!
Failures:1) Protractor Demo App should print correct logs Message: Failed: No element found using locator: By(css selector, *[id="MISTAKE"])
52
Паззлер №2: Чиним
function log(text) { protractor.promise.controlFlow().execute( function () { console.log(text); } );}
53
Паззлер №2
function doInputAndClick(nick, age) { element(by.css('.input-nick')).sendKeys(nick); log('INSERTED NICK'); element(by.id('MISTAKE')).click(); element(by.css('.input-age')).sendKeys(age); log('INSERTED AGE'); element(by.css('.input-submit')).click(); log('CLICKED THE BUTTON');}
54
Итоги по асинхронности Сложно, но неизбежно
Используйте async/await по возможности (Node 7.6+)
В основе всё равно лежат промисы, про них надо знать
Пока что есть не у всех, поддержку control flow можно включать/выключать флагомpromise.USE_PROMISE_MANAGER = true/false
Больше информации тут: https://github.com/SeleniumHQ/selenium/issues/2969
55
Паззлер №3
56
Паззлер №3
it('should have correct title on login page', function() { element(by.linkText('Login')).click();
expect(browser.getTitle()).toEqual('Login page');});
57
Паззлер №3
it('should have correct title on login page', function() { element(by.linkText('Login')).click();
expect(browser.getTitle()).toEqual('Login page');});
Вариант 1: Всё прекрасно, что тут может пойти не так???
58
Паззлер №3
it('should have correct title on login page', function() { element(by.linkText('Login')).click();
expect(browser.getTitle()).toEqual('Login page');});
Вариант 2: linkText – нестандартный селектор, он не будет работать в Angular 4
59
Паззлер №3
it('should have correct title on login page', function() { element(by.linkText('Login')).click();
expect(browser.getTitle()).toEqual('Login page');});
Вариант 3: Страница логина – не на Angular, Protractor не сможет на неё перейти
60
Паззлер №3
it('should have correct title on login page', function() { element(by.linkText('Login')).click();
expect(browser.getTitle()).toEqual('Login page');});
Вариант 4: Страница логина – не на Angular, Protractor не сможет получить её заголовок
61
Паззлер №3
it('should have correct title on login page', function() { element(by.linkText('Login')).click();
expect(browser.getTitle()).toEqual('Login page');});
Вариант 1: Всё прекрасно, что тут может пойти не так???Вариант 2: linkText – нестандартный селектор, он не будет работать в Angular 4Вариант 3: Страница логина – не на Angular, Protractor не сможет на неё перейтиВариант 4: Страница логина – не на Angular, Protractor не сможет получить её заголовок
62
Паззлер №3: Запускаем....
StartedFail.Failures:1) Protractor Demo App should have correct title on login page Message: Failed: Error while waiting for Protractor to sync with the page: "window.angular is undefined."
63
Паззлер №3: Чиним
it('should have correct title on login page', function() { element(by.linkText('Login')).click(); browser.ignoreSynchronization = true;
expect(browser.getTitle()).toEqual('Login page');});
64
Итого
65
Итого Protractor поможет написать тесты значительно быстрее
Но (в связке с JavaScript) может преподнести немало сюрпризов
Надо знать инструмент, которым пользуешься
66
«У нас уже есть куча тестов на Java, да и страшно как-то после паззлеров писать на JS.
Что делать?»
67
Вспомним тест на Java
@Testpublic void testResultAddedToHistory() throws Exception {
final List<WebElement> inputs = driver.findElements(By.tagName("input"));
inputs.get(0).sendKeys("Korobochka");inputs.get(1).sendKeys("17");driver.findElement(By.className("btn")).click();new WebDriverWait(driver, 10).until(webDriver ->
!webDriver.findElement(By.tagName("h3")).getText() .contains("Loading"));
WebElement targetElement = driver.findElement( By.xpath("//tbody/tr[1]/td[last()]"));
assertEquals(targetElement.getText(), "Korobochka2000");}
68
Разбираемся с реализацией
browser.waitForAngular();
69
Разбираемся ... (browser.js)
waitForAngular(opt_description) { if (this.ignoreSynchronization) { return true; } let runWaitForAngularScript = () => { return this.angularAppRoot().then((rootEl) => { return this.executeAsyncScript_( clientSideScripts.waitForAngular, rootEl); }); }; return runWaitForAngularScript()...
70
Разбираемся ... (clientsidescripts.js)
/** * All scripts to be run on the client via executeAsyncScript or * executeScript should be put here. * * NOTE: These scripts are transmitted over the wire * as JavaScript text constructed using their toString * representation, and *cannot* reference external variables. */
71
Разбираемся ... (clientsidescripts.js) waitForAngular
findBindings
findRepeaterRows
findAllRepeaterRows
findRepeaterElement
findByModel
findByOptions
findByButtonText
findByCssContainingText
testForAngular
72
Разбираемся ... (clientsidescripts.js)functions.waitForAngular = function(rootSelector, callback) { try { if (window.angular && !(window.angular.version && window.angular.version.major > 1)) { ... } else if (rootSelector && window.getAngularTestability) { ... } else if (window.getAllAngularTestabilities) { ... } else if (!window.angular) { throw new Error('window.angular is undefined. ...') } } catch (err) { callback(err.message); }};
73
Разбираемся ... (clientsidescripts.js)functions.waitForAngular = function(rootSelector, callback) { ... if (window.getAllAngularTestabilities) { var testabilities = window.getAllAngularTestabilities(); var count = testabilities.length; var decrement = function () { count--; if (count === 0) { callback(); } }; testabilities.forEach(function (testability) { testability.whenStable(decrement); }); } ...};
74
Применяем в Java
75
Итоги
76
Используйте подходящий задаче инструмент Protractor очень хорошо подходит для тестирования Angular
приложений
Если есть возможность – используйте!
77
Изучайте инструменты На примере выше мы подсмотрели, как работает Protractor, и
улучшили свои Java тесты
Так можно делать со многими вещами
Иногда даже имеет смысл написать свой инструмент
78
Сложные программы и фреймворки должны предоставлять инструменты для тестирования
Тестирование – это не только поиск багов, но и сокращение расходов на разработку в целом
Даже небольшой вклад от разработчиков может удешевить процесс тестирования
Пользуйтесь API для тестирования, если они есть!
79
Q&A
Илья Коробицын mailto: [email protected] Telegram: @korobochka
Код: https://github.com/korobochka/heisenbug-protractor
80
81
Backup slides
82
Angular Testabilities
> getAllAngularTestabilities()[0].isStable();true
83
Universal waiter
public void waitForAnguar() {new WebDriverWait(driver, 10).until(webDriver ->
((JavascriptExecutor)webDriver).executeScript( "return getAllAngularTestabilities()0].isStable();" ).equals(Boolean.TRUE))}