Protractor是一种流行的端到端测试框架,它使您可以在真实的浏览器中测试Angular应用程序,从而模拟浏览器的交互,就像真实用户与之交互一样。 端到端测试旨在确保应用程序的行为符合用户的预期。 而且,测试不关心实际的代码实现。
量角器在流行的Selenium WebDriver之上运行,后者是用于浏览器自动化和测试的API。 除了Selenium WebDriver提供的功能外,Protractor还提供了用于捕获Angular应用程序的UI组件的定位器和方法。
在本教程中,您将了解:
- 设置,配置和运行量角器
- 为量角器编写基本测试
- 页面对象以及为什么要使用它们
- 编写测试时要考虑的准则
- 从头到尾为应用程序编写端到端测试
这听起来不令人兴奋吗? 但是,首先是第一件事。
我需要使用量角器吗?
如果您一直在使用Angular-CLI,则可能知道默认情况下,它附带了两个用于测试的框架。 他们是:
- 使用Jasmine和Karma进行单元测试
- 使用量角器进行端到端测试
两者之间的明显区别在于,前者用于测试组件和服务的逻辑,而后者用于确保应用程序的高级功能(涉及UI元素)按预期工作。
如果您不熟悉Angular中的测试,建议您阅读“使用Jasmine进行Angular中的测试组件”系列,以更好地了解在哪里画线。
在前者的情况下,您可以利用Angular测试实用程序和Jasmine的功能来编写组件和服务的单元测试,还可以编写基本的UI测试。 但是,如果您需要从头到尾测试应用程序的前端功能,则可以使用Protractor。 量角器的API与页面对象等设计模式相结合,使编写更具可读性的测试变得更加容易。 这是使事情进展的示例。
/*
1. It should have a create Paste button
2. Clicking the button should bring up a modal window
*/
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");
});
配置量角器
如果您使用Angular-CLI生成项目,则设置Protractor很容易。 ng new
创建的目录结构如下。
.
├── e2e
│ ├── app.e2e-spec.ts
│ ├── app.po.ts
│ └── tsconfig.e2e.json
├── karma.conf.js
├── package.json
├── package-lock.json
├── protractor.conf.js
├── README.md
├── src
│ ├── app
│ ├── assets
│ ├── environments
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.css
│ ├── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ └── typings.d.ts
├── tsconfig.json
└── tslint.json
5 directories, 19 files
Protractor创建的默认项目模板取决于两个文件来运行测试: e2e目录中的spec文件和配置文件( 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: 'https://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网络浏览器上运行该测试,则可以保持原样,并跳过本节的其余部分。
使用Selenium独立服务器设置量角器
directConnect: true
允许量角器直接连接到浏览器驱动程序。 但是,在撰写本教程时,Chrome是唯一受支持的浏览器。 如果您需要多浏览器支持或运行Chrome以外的浏览器,则必须设置Selenium独立服务器。 步骤如下。
使用npm全局安装Protractor:
npm install -g protractor
这将为webdriver-manager以及量角器安装命令行工具。 现在更新webdriver-manager以使用最新的二进制文件,然后启动Selenium独立服务器。
webdriver-manager update
webdriver-manager start
最后,将directConnect: false
设置为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每次运行ng e2e
时都必须编译代码。 如果您想加快速度,这就是您应该做的。 使用ng serve
服务应用程序。
然后启动一个新的控制台选项卡并运行:
ng e2e -s false
测试现在应该加载得更快。
我们的目标
我们将为基本的Pastebin应用程序编写E2E测试。 从GitHub存储库克隆项目。
这两个版本,入门版(没有测试的版本)和最终版本(有测试的版本)在单独的分支上可用。 现在,克隆入门分支。 (可选)为项目提供服务并遍历代码以熟悉手头的应用程序。
让我们简要地描述我们的Pastebin应用程序。 该应用程序最初会将粘贴列表(从模拟服务器检索)粘贴到表中。 表格中的每一行都有一个“ 查看粘贴”按钮,单击该按钮会打开一个引导模式窗口。 模态窗口显示粘贴数据以及用于编辑和删除粘贴的选项。 在表格末尾,有一个“ 创建粘贴”按钮,可用于添加新的粘贴。
本教程的其余部分专用于在Angular中编写量角器测试。
量角器基础
以.e2e-spec.ts结尾的spec文件将托管我们应用程序的实际测试。 我们将所有测试规范放置在e2e目录中,因为这是我们将Protractor配置为查找规范的地方。
编写量角器测试时,需要考虑两件事:
- 茉莉花语法
- 量角器API
茉莉花语法
使用以下代码创建一个名为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组件有关的所有规范都应包含在标题为Pastebin Page的描述块内。 尽管这可能会导致多余的测试,但是您的测试将更具可读性和可维护性。
describe块可以具有beforeEach()
方法,该方法将在该块中的每个规范之前执行一次。 因此,如果您需要浏览器在每次测试之前导航到URL,则将导航代码放在beforeEach()
是正确的做法。
接受值的Expect语句与一些匹配器函数链接在一起。 比较实际值和期望值,并返回一个布尔值,该布尔值确定测试是否失败。
量角器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 API导出的主要组件。
-
browser()
:您应该为所有浏览器级别的操作(例如导航,调试等browser()
调用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')
:基于内部文本定位按钮元素或按钮元素数组。
注意:在编写本教程时,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', 'Ruby')).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");
});
上面的代码有效,您可以自己验证。 但是,如果在spec文件中没有针对量角器的词汇,您会不会更轻松地编写测试? 这就是我在说的:
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);
//Now update 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");
});
没有多余的量角器包,这些规格看起来更加简单。 我是怎么做到的? 让我向您介绍页面对象。
页面对象
Page Object是一种设计模式,在测试自动化界很流行。 页面对象使用面向对象的类为应用程序的页面或部分建模。 可以将所有对象(与我们的测试相关),例如文本,标题,表格,按钮和链接都捕获在页面对象中。 然后,我们可以将这些页面对象导入规范文件并调用其方法。 这减少了代码重复,并使代码维护更加容易。
创建一个名为page-objects的目录,并在其中添加一个名为pastebin.po.ts的新文件。 与Pastebin组件有关的所有对象都将在此处捕获。 如前所述,我们将整个应用程序分为三个不同的组件,每个组件都有一个专用于它的页面对象。 .po.ts的命名方式纯属常规,您可以根据需要命名。
这是 我们正在测试的页面。
这是代码。
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<any> {
return browser.get('/');
}
getPastebin():ElementFinder {
return element(by.css('.pastebin'));
}
/* Pastebin Heading */
getPastebinHeading(): promise.Promise<string> {
return this.getPastebin().element(by.css("h2")).getText();
}
/*Table Data */
getTable():ElementFinder {
return this.getTable().element(by.css('table'));
}
getTableHeader(): promise.Promise<string> {
return this.getPastebin().all(by.tagName('tr')).get(0).getText();
}
getTableRow(): ElementArrayFinder {
return this.getPastebin().all(by.tagName('tr'));
}
getFirstRowData(): promise.Promise<string> {
return this.getTableRow().get(1).getText();
}
getLastRowData(): promise.Promise<string> {
return this.getTableRow().last().getText();
}
/*app-add-paste tag*/
getAddPasteTag(): ElementFinder {
return this.getPastebin().element(by.tagName('app-add-paste'));
}
isAddPasteTagPresent(): promise.Promise<boolean> {
return this.getAddPasteTag().isPresent();
}
}
让我们回顾一下到目前为止所学到的知识。 量角器的API返回对象,到目前为止,我们已经遇到了三种类型的对象。 他们是:
- 承诺
- 元素查找器
- 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测试分组。 以与项目结构相匹配的方式组织测试。
- 如果有多个页面,则页面对象应具有自己的单独目录。
- 如果页面对象有一些共同的方法(例如
navigateToHome()
),请创建一个基本页面对象。 其他页面模型可以从基本页面模型继承。 - 使您的测试彼此独立。 您不希望由于UI的微小更改而导致所有测试失败,是吗?
- 使页面对象定义没有断言/期望。 断言应在spec文件中进行。
按照上述准则,页面对象层次结构和文件组织应如下所示。
我们已经介绍了pastebin.po.ts和mainPage.e2e-spec.ts 。 这是其余文件。
基本页面对象
/* path: e2e/page-objects/base.po.ts */
import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from 'protractor';
export class Base {
/* Navigational methods */
navigateToHome():promise.Promise<any> {
return browser.get('/');
}
navigateToAbout():promise.Promise<any> {
return browser.get('/about');
}
navigateToContact():promise.Promise<any> {
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"));
}
}
添加粘贴页面对象
/* 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<boolean> {
return this.getCreateButton().isPresent();
}
clickCreateButton(): promise.Promise<void> {
return this.getCreateButton().click();
}
/*Create Paste Modal */
getCreatePasteModal(): ElementFinder {
return this.getAddPaste().element(by.id("source-modal"));
}
isCreatePasteModalPresent() : promise.Promise<boolean> {
return this.getCreatePasteModal().isPresent();
}
/*Save button */
getSaveButton(): ElementFinder {
return this.getAddPaste().element(by.buttonText("Save"));
}
clickSaveButton():promise.Promise<void> {
return this.getSaveButton().click();
}
/*Close button */
getCloseButton(): ElementFinder {
return this.getAddPaste().element(by.buttonText("Close"));
}
clickCloseButton():promise.Promise<void> {
return this.getCloseButton().click();
}
/* Get Input Paste values from the Modal window */
getInputPasteValues(): Promise<string[]> {
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]);
}
}
添加粘贴规范文件
/* 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 modal window
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");
});
});
练习题
但是,还缺少一些内容:“ 查看粘贴”按钮的测试和单击该按钮后弹出的模式窗口。 我将把它作为练习留给您。 但是,我会给您一个提示。
页面对象的结构和ViewPastePage的规范与AddPastePage的相似。
这是您需要测试的方案:
- ViewPaste页面上应该有一个按钮,并且在单击时应该会弹出一个模式窗口。
- 模态窗口应显示最近添加的粘贴的粘贴数据。
- 模态窗口应允许您更新值。
- 删除按钮应该起作用。
尽可能尝试遵守准则。 如果您有疑问,请切换到final分支以查看代码的最终草案。
包起来
所以你有它。 在本文中,我们介绍了使用Protractor为Angular应用程序编写端到端测试。 我们首先讨论了单元测试与e2e测试,然后学习了有关设置,配置和运行Protractor的知识。 本教程的其余部分集中于为演示Pastebin应用程序编写实际测试。
请让我知道您对使用量角器编写测试或为Angular编写测试的想法和经验。 我很想听听他们的声音。 谢谢阅读!