Знайомство з комплексним тестуванням Angular з допомогою Protractor

3

Від автора: Protractor – популярний фреймворк комплексного тестування. З допомогою Protractor Angular додаток можна тестувати в реальному браузері, імітуючи взаємодії як з реальним користувачем. Комплексне тестування перевіряє, щоб додаток вело себе рівно так, як очікує користувач. Крім цього, тести безпосередньо не відносяться до коду.

Знайомство з комплексним тестуванням Angular з допомогою Protractor

Protractor запускається поверх популярного Selenium WebDriver (API для автоматизації браузера і тестування). Крім функцій Selenium WebDriver Protractor пропонує локатори і методи для захоплення UI компонентів додатка Angular. У цьому уроці ви дізнаєтеся про:

Установки, настройки і запуску Protractor

Написання базових тестів для Protractor

Об’єктах сторінок і про те, як їх використовувати

Посібниках для написання тестів

Написанні E2E тестів для програми з нуля і до самого кінця

Звучить круто? Але спочатку про головне.

Чи потрібно мені використовувати Protractor?

Якщо ви працювали з Angular CLI, то ви повинні знати, що за замовчуванням з ним йде 2 фреймворку для тестування. І це:

Юніт тести на Jasmine і Karma

Комплексні тести на Protractor

Основна відмінність – перший використовується для тестування логіки компонентів і сервісів, а другий використовується для перевірки функціональності високого рівня програми (в тому числі і елементи UI).

Якщо ви жодного разу не тестували в Angular, рекомендую почитати серію статей «тестування компонентів у Angular на Jasmine», щоб краще зрозуміти принцип.

У першому випадку можна використовувати міць тестових утиліт Angular і Jasmine для написання не просто юніт тестів для компонентів і сервісів, а також для написання базових UI тестів. Однак якщо необхідно протестувати front end функціонал програми повністю, Protractor вам в руки. Protractor API поєднаний з шаблонами проектування, такими як об’єкти сторінок, що спрощує написання тестів і робить їх більш чіткими. Приклад.

/*
1. It should have a create Paste button
2. Clicking the button should bring up a window modal
*/
it(‘should have a Create Paste button and modal window’, () => {
expect(addPastePage.isCreateButtonPresent()).toBeTruthy(«The button should exist»);
expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy(«The modal window shouldn’t exist, not yet!»);
addPastePage.clickCreateButton();
expect(addPastePage.isCreatePasteModalPresent()).toBeTruthy(«The modal window should appear now»);
});

Налаштування Protractor

Якщо для генерації проекту ви використовуєте Angular CLI, налаштування Protractor не викличе утруднень. Команду ng new створює таку структуру папок.

Знайомство з комплексним тестуванням Angular з допомогою Protractor

Стандартний шаблон проекту, створений Protractor, залежить від двох файлів для запуску тестів: спеціальні файли з папки e2e і файл конфігурацій (protractor.conf.js). Давайте подивимося, як налаштовується файл protractor.conf.js:

/* Path: protractor.conf.ts*/
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require(‘jasmine-spec-reporter’);
exports.config = {
allScriptsTimeout: 11000,
specs: [
‘./e2e/**/*.e2e-spec.ts’
],
capabilities: {
‘browserName’: ‘chrome’
},
directConnect: true,
baseUrl: ‘http://localhost:4200/’,
framework: ‘jasmine’,
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require(‘ts-node’).register({
project: ‘e2e/tsconfig.e2e.json’
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

Якщо ви вмієте запускати тести в Chrome, то можете залишити все, як є і пропустити частину розділу.

Налаштування Protractor з Selenium Standalone Server

Запис directConnect: true підключає Protractor безпосередньо до драйверів браузера. Однак на момент написання цього уроку Chrome – єдиний підтримуваний браузер. Якщо потрібна підтримка декількох браузерів або інший браузер, вам знадобиться встановити Selenium standalone server. Що для цього потрібно зробити.

Встановіть Protractor глобально з допомогою npm:

npm install -g protractor

Це встановить інструмент командного рядка для вебдрайвер менеджера разом з Protractor. Тепер оновіть вебдрайвер менеджер на використання останніх дистрибутивів і запустіть Selenium standalone server.

webdriver-update manager
webdriver-start manager

Встановіть directConnect: false і додайте властивість seleniumAddress:

capabilities: {
‘browserName’: ‘firefox’
},
directConnect: false,
baseUrl: ‘http://localhost:4200/’,
seleniumAddress: ‘http://localhost:4444/wd/hub’,
framework: ‘jasmine’,
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},

Конфіг файл на GitHub дає більше інформації про варіанти установки в Protractor. Я в уроці буду використовувати стандартні опції.

Запуск тестів

Якщо ви працюєте в Angular CLI, то для запуску тестів вам потрібна команда ng e2e. Якщо здається, що тести повільні, це тому що Angular має компілювати код при кожному запуску e2e. Якщо хочете трохи прискорити їх, вам потрібно зробити наступне. Запустіть ng serve. Відкрийте нову вкладку в консолі і запустіть:

ng e2e -s false

Тепер тести повинні завантажуватися швидше.

Наша мета

Ми будемо писати E2E тести для стандартного додатка Pastebin. Клонуйте об’єкт з репозиторію GitHub. Обидві версії (стартова без тестів і фінальна з тестами) доступні в окремих гілках. Клонуйте гілку starter. Можете пробігтися по коду, щоб ознайомитись з додатком.

Давайте коротко опишемо наше додаток Pastebin. Додаток спочатку буде завантажувати список вставок (отриманих з помилкового сервера) в таблицю. У кожному рядку таблиці буде кнопка View Paste, при натисканні на яку буде відкриватися модальний первинної завантаження. Модальне вікно відображає дані вставки з опціями редагування і видалення. В кінці таблиці є кнопка Create Paste, з допомогою якої можна додати нові вставки.

Знайомство з комплексним тестуванням Angular з допомогою Protractor

Частина уроку присвячена написанню Protractor тестів в Angular.

Основи Protractor

У файлі специфікації, який закінчується на .e2e-spec.ts, будуть зберігатися реальні тести програми. Всі тестові специфікації будуть поміщатися в папку e2e, так як ми налаштували, щоб Protractor шукав специфікації саме в цьому місці. При написанні тестів на Protractor необхідно врахувати 2 речі:

Синтаксис Jasmine

Protractor API

Синтаксис Jasmine

Створіть новий файл test.e2e-spec.ts з кодом.

/* Path: e2e/test.e2e-spec.ts */
import { browser, by element } from ‘protractor’;
describe(‘Protractor Demo’, () => {
beforeEach(() => {
//The code here will get executed before each it block is called
//browser.get(‘/’);
});
it(‘should display the name of the application’,() => {
/*Expectations accept parameters that will be matched with the real value
using Jasmine’s matcher functions. eg. toEqual(),toContain(), toBe(), toBeTruthy() etc. */
expect(«Pastebin Application»).toEqual(«Pastebin Application»);
});
it(‘should click the create Paste button’,() => {
//spec goes here
});
});

Тут описується, як наші тести будуть організовані у файлі специфікації за допомогою синтаксису Jasmine. describe(), beforeEach() і it() – глобальні функції Jasmine.

У Jasmine хороший синтаксис для написання тестів, і він відмінно працює з Protractor. Якщо ви не знайомі з Jasmine, рекомендую спочатку зайти на сторінку Jasmine на GitHub.

Блок describe розбиває тести на логічні сьюти. У кожному блоці describe (або тест сьют) може бути кілька блоків it (тестових специфікацій). Самі тести пишуться в тестових специфікаціях.

Ви запитаєте: «навіщо мені робити таку структуру в тестах?». Тест сьют логічно описує окрему функцію додатка. Наприклад, всі тестові специфікації по компоненту Pastebin повинні, в ідеалі, бути покриті в блоці describe з заголовком Pastebin Page. Однак це може призвести до надмірності тестів, але зробить їх більш чіткими і обслуговуваними.

У блоці describe може бути метод beforeEach(), який запускається один раз перед кожною специфікацією в цьому блоці. Тобто якщо вам перед кожним тестом необхідно редірект браузер на заданий URL, додайте редирект всередині beforeEach().

Вирази expect приймають значення, зчеплені з функціями пошуку. Реальне та очікуване значення порівнюються, і повертає логічне значення, що вказує, провалився тест чи ні.

Protractor API

Давайте «обтянем наш скелет плоттю».

/* Path: e2e/test.e2e-spec.ts */
import { browser, by element } from ‘protractor’;
describe(‘Protractor Demo’, () => {
beforeEach(() => {
browser.get(‘/’);
});
it(‘should display the name of the application’,() => {
expect(element(by.css(‘.pastebin’)).getText()).toContain(‘Pastebin Application’);
});
it(‘create Paste button should work’,() => {
expect(element(by.id(‘source-modal’)).isPresent()).toBeFalsy(«The modal window shouldn’t appear right now «);
element(by.buttonText(‘create Paste’)).click();
expect(element(by.id(‘source-modal’)).isPresent()).toBeTruthy(‘The modal window should appear now’);
});
});

browser.get(‘/’) і element(by.css(‘.pastebin’)).getText() – частина Protractor API. Давайте испачкаем руки і запрыгнем в Protractor. Популярні компоненти з Protractor API перераховані нижче.

Browser(): викликається для всіх операцій на рівні браузера, наприклад, навігація, дебагінг і т. д.

Element(): використовується для пошуку елемента в DOM за умовою пошуку або зв’язки умов. Повертається об’єкт ElementFinder, з ним можна виконувати операції типу getText() або click().

element.all(): шукає масив елементів по зв’язці умов. Повертає об’єкт ElementArrayFinder. До ElementArrayFinder застосовні всі операції від ElementFinder.

Локатори: з допомогою локаторів в додатку Angular можна шукати елементи.

Ми дуже часто будемо використовувати локатори, тому ось исползуемые:

By.css(‘selector-name’): самий часто використовуваний локатор для пошуку елемента за назвою CSS селектора

by.name(‘name value’): виявляє елемент по значенню атрибута name

by.buttonText(‘button value’): знаходить елемент button або масив button по внутрішньому тексту

Зверніть увагу: локатори by.model by.binding і by.repeater не працюють у додатках Angular 2+ на момент написання уроку. Використовуйте CSS локатори. Напишемо тести для нашого додатка Pastebin.

it(‘should accept and save input values’, () => {
element(by.buttonText(‘create Paste’)).click();
//send input values to the form using sendKeys
element(by.name(‘title’)).sendKeys(‘Hello world in Ruby’);
element(by.name(‘language’)).element(by.cssContainingText(‘option’, ‘Рубін’)).click();
element(by.name(‘paste’)).sendKeys(«puts ‘Hello world’;»);
element(by.buttonText(‘Save’)).click();
//expect the table to contain the new paste
const lastRow = element.all(by.tagName(‘tr’)).last();
expect(lastRow.getText()).toContain(«Hello world in Ruby»);
});

Код вище працює, можете самі перевірити. Однак не буде зручніше писати тести без словника Protractor у файлі специфікації? Про що я кажу:

it(‘should have an Create Paste button and modal window’, () => {
expect(addPastePage.isCreateButtonPresent()).toBeTruthy(«The button should exist»);
expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy(«The modal window shouldn’t appear, not yet!»);
addPastePage.clickCreateButton();
expect(addPastePage.isCreatePasteModalPresent()).toBeTruthy(«The modal window should appear now»);
});
it(‘should accept and save input values’, () => {
addPastePage.clickCreateButton();
//Input field should be empty initially
const emptyInputValues = [«»,»»,»»];
expect(addPastePage.getInputPasteValues()).toEqual(emptyInputValues);
//Update Now the input fields
addPastePage.addNewPaste();
addPastePage.clickSaveButton();
expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy(«The modal window should be gone»);
expect(mainPage.getLastRowData()).toContain(«Hello World in Ruby»);
});

Специфікації здаються простіше без додаткового сміття від Protractor. Як я це зробив? Дозвольте познайомити вас із Page Objects.

Page Objects

Page Objects – шаблон проектування, популярний серед автоматизаторів кейсів. Об’єкт сторінки моделює сторінку або частину додатка за допомогою об’єктно-орієнтованого класу. Всі об’єкти (нашого тесту), такі як текст, заголовки, таблиці, кнопки та посилання захоплюються в об’єкт сторінки. Ці об’єкти сторінок можна імпортувати файл специфікації і виконувати їх методи. Це знижує повторення в коді і спрощує його обслуговування.

Створіть папку page-objects і додайте новий файл pastebin.po.ts. Всі об’єкти компонента Pastebin будуть тут. Як я вже сказав, ми розбили всі додатки на 3 компонента, і в кожного компонента об’єкт сторінки. Схема іменування .po.ts, імена можете придумати, які завгодно.

Макет сторінки для тестування.

Знайомство з комплексним тестуванням Angular з допомогою Protractor

Код pastebin.po.ts

/* Path e2e/page-objects/pastebin.po.ts*/
import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from ‘protractor’;
export class Pastebin extends Base {
navigateToHome():promise.Promise {
return browser.get(‘/’);
}
getPastebin():ElementFinder {
return element(by.css(‘.pastebin’));
}
/* Pastebin Heading */
getPastebinHeading(): promise.Promise {
return this.getPastebin().element(by.css(«h2»)).getText();
}
/*Table Data */
getTable():ElementFinder {
return this.getTable().element(by.css(‘table’));
}
getTableHeader(): promise.Promise {
return this.getPastebin().all(by.tagName(‘tr’)).get(0).getText();
}
getTableRow(): ElementArrayFinder {
return this.getPastebin().all(by.tagName(‘tr’));
}
getFirstRowData(): promise.Promise {
return this.getTableRow().get(1).getText();
}
getLastRowData(): promise.Promise {
return this.getTableRow().last().getText();
}
/*app-add-paste tag*/
getAddPasteTag(): ElementFinder {
return this.getPastebin().element(by.tagName(‘app-add-paste’));
}
isAddPasteTagPresent(): promise.Promise {
return this.getAddPasteTag().isPresent();
}
}

Що ми вже знаємо. Protractor API повертає об’єкти. Поки що ми працювали з об’єктами трьох типів. Це:

promise.Promise

ElementFinder

ElementArrayFinder

Якщо коротко, element() повертає ElementFinder, element().all повертає ElementArrayFinder. З допомогою локаторів (by.css, by.tagName і т. д.) можна шукати положення елемента в DOM і передавати його в element() або element.all().

ElementFinder і ElementArrayFinder можна зчепити діями, наприклад, isPresent(), getText(), click() і т. д. Ці метод повертають промис, який дозволяється, коли завершується певну дію.

Чому у нас в тесті немає ланцюга then() – тому що Protractor робить це всередині. Тести здаються синхронними, хоча це не так. Тому наш код буде лінійним. Але я рекомендую використовувати синтаксис async/await , щоб захистити код.

Нижче показано, як можна зчепити об’єкти ElementFinder. Це дуже зручно, якщо в DOM є кілька селекторів на одне ім’я, а нам потрібно вибрати правильний.

getTable():ElementFinder {
return this.getPastebin().element(by.css(‘table’));
}

Наш код для об’єкта сторінки готовий. Давайте імпортуємо його специфікацію. Код для первинних тестів.

/* Path: e2e/mainPage.e2e-spec.ts */
import { Pastebin } from ‘./page-objects/pastebin.po’;
import { browser, protractor } from ‘protractor’;
/* Scenarios to be Tested
1. Pastebin Page should display a heading with text Pastebin Application
2. It should have a table header
3. The table should have rows
4. app-add-paste tag should exist
*/
describe(‘Pastebin Page’, () => {
const mainPage: Pastebin = new Pastebin();
beforeEach(() => {
mainPage.navigateToHome();
});
it(‘should display the heading Pastebin Application’, () => {
expect(mainPage.getPastebinHeading()).toEqual(«Pastebin Application»);
});
it(‘should have a table header’, () => {
expect(mainPage.getTableHeader()).toContain(«id Title Language Code»);
})
it(‘table should have at least one row’, () => {
expect(mainPage.getFirstRowData()).toContain(«Hello world»);
})
it(‘should have the app-add-paste tag’, () => {
expect(mainPage.isAddPasteTagPresent()).toBeTruthy();
})
});

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

Тести повинні бути організовані так, щоб загальна структура виглядала просто і зрозуміло. Ось декілька порад, яких слід дотримуватися при організації E2E тестів.

Відокремте E2E тести від юніт тестів

Згрупуйте E2E тести розумно. Організуйте тести за структурою проекту

Якщо у вас багато сторінок, у об’єктів сторінок повинен бути окремий шлях

Якщо об’єкти сторінок ділять методи (наприклад navigateToHome()), створіть об’єкт базової сторінки. Моделі інших сторінок можуть успадковуватися від моделі базової сторінки

Зробіть тести незалежними один від одного. Вам не потрібно, щоб всі тести провалилися через невеликої зміни в UI, так адже?

Не використовуйте перевірки у визначеннях об’єктів сторінок. Перевірки повинні бути у файлі специфікації

Дотримуючись порад вище, ми отримуємо наступну ієрархію сторінок і організацію файлів.

Знайомство з комплексним тестуванням Angular з допомогою Protractor

Про файлах pastebin.po.ts і mainPage.e2e-spec.ts ми вже говорили. Нижче представлені залишилися файли. Base Page Object

/* path: e2e/page-objects/base.po.ts */
import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from ‘protractor’;
export class Base {
/* Navigational methods */
navigateToHome():promise.Promise {
return browser.get(‘/’);
}
navigateToAbout():promise.Promise {
return browser.get(‘/about’);
}
navigateToContact():promise.Promise {
return browser.get(‘/contact’);
}
/* Mock data for creating a new Paste and editing existing paste */
getMockPaste(): any {
let paste: any = { title: «Something here»,language: «Ruby»,paste: «Test»}
return paste;
}
getEditedMockPaste(): any {
let paste: any = { title: «Paste 2», language: «JavaScript», paste: «Test2» }
return paste;
}
/* Methods shared by addPaste and viewPaste */
getInputTitle():ElementFinder {
return element(by.name(«title»));
}
getInputLanguage(): ElementFinder {
return element(by.name(«language»));
}
getInputPaste(): ElementFinder {
return element(by.name(«paste»));
}
}

Об’єкт Add Page Paste

Знайомство з комплексним тестуванням Angular з допомогою Protractor

/* Path: e2e/page-objects/add-paste.po.ts */
import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from ‘protractor’;
import { Base } from ‘./base.po’;
export class AddPaste extends Base {
getAddPaste():ElementFinder {
return element(by.tagName(‘app-add-paste’));
}
/* Create Paste button */
getCreateButton(): ElementFinder {
return this.getAddPaste().element(by.buttonText(«create Paste»));
}
isCreateButtonPresent() : promise.Promise {
return this.getCreateButton().isPresent();
}
clickCreateButton(): promise.Promise {
return this.getCreateButton().click();
}
/*Create Paste Modal */
getCreatePasteModal(): ElementFinder {
return this.getAddPaste().element(by.id(«source-modal»));
}
isCreatePasteModalPresent() : promise.Promise {
return this.getCreatePasteModal().isPresent();
}
/*Save button */
getSaveButton(): ElementFinder {
return this.getAddPaste().element(by.buttonText(«Save»));
}
clickSaveButton():promise.Promise {
return this.getSaveButton().click();
}
/*Close button */
getCloseButton(): ElementFinder {
return this.getAddPaste().element(by.buttonText(«Close»));
}
clickCloseButton():promise.Promise {
return this.getCloseButton().click();
}
/* Get Input Paste values from the Modal window */
getInputPasteValues(): Promise {
let inputTitle, inputLanguage, inputPaste;
// Return the input values after the promise is resolved
// Note that this.getInputTitle().getText doesn’t work
// Use getAttribute(‘value’) instead
return Promise.all([this.getInputTitle().getAttribute(«value»), this.getInputLanguage().getAttribute(«value»), this.getInputPaste().getAttribute(«value»)])
.then( (values) => {
return values;
});
}
/* Add a new Paste */
addNewPaste():any {
let newPaste: any = this.getMockPaste();
//Send input values
this.getInputTitle().sendKeys(newPaste.title);
this.getInputLanguage()
.element(by.cssContainingText(‘option’, newPaste.language)).click();
this.getInputPaste().sendKeys(newPaste.paste);
//Convert the paste object into an array
return Object.keys(newPaste).map(key => newPaste[key]);
}
}

Файл Add Paste Spec

/* Path: e2e/addNewPaste.e2e-spec.ts */
import { Pastebin } from ‘./page-objects/pastebin.po’;
import { AddPaste } from ‘./page-objects/add-paste.po’;
import { browser, protractor } from ‘protractor’;
/* Scenarios to be Tested
1. AddPaste Page should have a button when clicked on should present a window modal
2. The modal window should accept the new values and save them
4. The saved data should appear in the MainPage
3. Close button should work
*/
describe(‘Add-New-Paste page’, () => {
const addPastePage: AddPaste = new AddPaste();
const mainPage: Pastebin = new Pastebin();
beforeEach(() => {
addPastePage.navigateToHome();
});
it(‘should have an Create Paste button and modal window’, () => {
expect(addPastePage.isCreateButtonPresent()).toBeTruthy(«The button should exist»);
expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy(«The modal window shouldn’t appear, not yet!»);
addPastePage.clickCreateButton();
expect(addPastePage.isCreatePasteModalPresent()).toBeTruthy(«The modal window should appear now»);
});
it(«should accept and save input values», () => {
addPastePage.clickCreateButton();
const emptyInputValues = [«»,»»,»»];
expect(addPastePage.getInputPasteValues()).toEqual(emptyInputValues);
const newInputValues = addPastePage.addNewPaste();
expect(addPastePage.getInputPasteValues()).toEqual(newInputValues);
addPastePage.clickSaveButton();
expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy(«The modal window should be gone»);
expect(mainPage.getLastRowData()).toContain(«Something here»);
});
it(«close button should work», () => {
addPastePage.clickCreateButton();
addPastePage.clickCloseButton();
expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy(«The modal window should be gone»);
});
});

Вправи

Ми дещо не врахували: тести для кнопки View Paste і модальне вікно, яке з’являється після натискання на неї. Це буде ваше вправу. Але я дам підказку. Структура об’єктів сторінки і специфікацій для ViewPastePage така ж, як для AddPastePage.

Знайомство з комплексним тестуванням Angular з допомогою Protractor

Вам необхідно протестувати наступні сценарії:

Сторінка ViewPaste повинна мати кнопку по кліку повинна з’являтися модалка

Модальне вікно повинно відображати дані останньої вставки

Модальне вікно повинно дозволяти оновлювати значення

Повинна працювати кнопка видалення

Спробуйте максимально дотримуватися порад. Якщо сумніваєтеся, перейдіть на фінальну гілку, щоб побачити кінцевий чернетка коду.

Висновок

От і все. У цій статті ми навчилися писати комплексні тести для Angular додатки з допомогою Protractor. Спочатку ми поговорили про юніт тести і e2e тестах, після чого вивчили установку, настройку і запуск Protractor. Частина уроку була віддана на написання тестів під демо додаток Pastebin.