Пишем Selenium тесты на JS для тестирования Angular: плюсы ... ·...

Preview:

Citation preview

Гейзенбаг, 2017-06-04 Илья Коробицын

Пишем Selenium тесты на JS для тестирования Angular: плюсы,

минусы, подводные камни

2

О докладчике Илья Коробицын

mailto: talks@korobochka.org 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: talks@korobochka.org 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))}