Skip to main content

测试

使用 Ionic CLI 生成 @ionic/angular 应用时,会自动设置该应用以进行单元测试和端到端测试。这与 Angular CLI 使用的设置相同。有关测试 Angular 应用的详细信息,请参阅 角度测试指南

¥When an @ionic/angular application is generated using the Ionic CLI, it is automatically set up for unit testing and end-to-end testing of the application. This is the same setup that is used by the Angular CLI. Refer to the Angular Testing Guide for detailed information on testing Angular applications.

测试原理

¥Testing Principles

测试应用时,最好记住测试可以显示系统中是否存在缺陷。然而,不可能证明任何重要的系统完全没有缺陷。因此,测试的目的不是验证代码是否正确,而是发现代码中的问题。这是一个微妙但重要的区别。

¥When testing an application, it is best to keep in mind that testing can show if defects are present in a system. However, it is impossible to prove that any non-trivial system is completely free of defects. For this reason, the goal of testing is not to verify that the code is correct but to find problems within the code. This is a subtle but important distinction.

如果我们着手证明代码是正确的,我们就更有可能坚持通过代码的快乐之路。如果我们着手发现问题,我们就更有可能更充分地运用代码并发现潜伏在那里的错误。

¥If we set out to prove that the code is correct, we are more likely to stick to the happy path through the code. If we set out to find problems, we are more likely to more fully exercise the code and find the bugs that are lurking there.

最好从一开始就开始测试应用。这样可以在流程的早期发现缺陷,从而更容易修复它们。当新功能添加到系统中时,这也使得可以放心地重构代码。

¥It is also best to begin testing an application from the very start. This allows defects to be found early in the process when they are easier to fix. This also allows code to be refactored with confidence as new features are added to the system.

单元测试

¥Unit Testing

单元测试与系统的其余部分隔离地执行单个代码单元(组件、页面、服务、管道等)。隔离是通过注入模拟对象来代替代码的依赖来实现的。模拟对象允许测试对依赖的输出进行细粒度控制。模拟还允许测试确定哪些依赖已被调用以及哪些依赖已传递给它们。

¥Unit tests exercise a single unit of code (component, page, service, pipe, etc) in isolation from the rest of the system. Isolation is achieved through the injection of mock objects in place of the code's dependencies. The mock objects allow the test to have fine-grained control of the outputs of the dependencies. The mocks also allow the test to determine which dependencies have been called and what has been passed to them.

编写良好的单元测试的结构使得代码单元及其包含的功能通过 describe() 回调进行描述。通过 it() 回调测试代码单元及其功能的要求。当阅读 describe()it() 回调的描述时,它们作为一个短语是有意义的。当嵌套 describe() 和最终 it() 的描述连接在一起时,它们形成一个完整描述测试用例的句子。

¥Well-written unit tests are structured such that the unit of code and the features it contains are described via describe() callbacks. The requirements for the unit of code and its features are tested via it() callbacks. When the descriptions for the describe() and it() callbacks are read, they make sense as a phrase. When the descriptions for nested describe()s and a final it() are concatenated together, they form a sentence that fully describes the test case.

由于单元测试独立地执行代码,因此它们快速、健壮,并且允许高度的代码覆盖率。

¥Since unit tests exercise the code in isolation, they are fast, robust, and allow for a high degree of code coverage.

使用模拟

¥Using Mocks

单元测试单独执行代码模块。为了促进这一点,我们建议使用 Jasmine (https://jasmine.github.io/)。Jasmine 创建模拟对象(Jasmine 称之为 "spies")来代替测试时的依赖。使用模拟对象时,测试可以控制调用该依赖返回的值,从而使当前测试独立于对依赖所做的更改。这也使得测试设置更容易,允许测试仅关注被测模块内的代码。

¥Unit tests exercise a code module in isolation. To facilitate this, we recommend using Jasmine (https://jasmine.github.io/). Jasmine creates mock objects (which Jasmine calls "spies") to take the place of dependencies while testing. When a mock object is used, the test can control the values returned by calls to that dependency, making the current test independent of changes made to the dependency. This also makes the test setup easier, allowing the test to only be concerned with the code within the module under test.

使用模拟还允许测试查询模拟以确定它是否被调用以及如何通过 toHaveBeenCalled* 函数集调用它。测试应尽可能具体地使用这些函数,在测试某个方法已被调用时,倾向于调用 toHaveBeenCalledTimes 而不是调用 toHaveBeenCalled。即 expect(mock.foo).toHaveBeenCalledTimes(1) 优于 expect(mock.foo).toHaveBeenCalled()。当测试某些内容尚未被调用时,应遵循相反的建议(expect(mock.foo).not.toHaveBeenCalled())。

¥Using mocks also allows the test to query the mock to determine if it was called and how it was called via the toHaveBeenCalled* set of functions. Tests should be as specific as possible with these functions, favoring calls to toHaveBeenCalledTimes over calls to toHaveBeenCalled when testing that a method has been called. That is expect(mock.foo).toHaveBeenCalledTimes(1) is better than expect(mock.foo).toHaveBeenCalled(). The opposite advice should be followed when testing that something has not been called (expect(mock.foo).not.toHaveBeenCalled()).

在 Jasmine 中创建模拟对象有两种常见方法。可以使用 jasmine.createSpyjasmine.createSpyObj 从头开始构建模拟对象,也可以使用 spyOn()spyOnProperty() 将间谍安装到现有对象上。

¥There are two common ways to create mock objects in Jasmine. Mock objects can be constructed from scratch using jasmine.createSpy and jasmine.createSpyObj or spies can be installed onto existing objects using spyOn() and spyOnProperty().

使用 jasmine.createSpyjasmine.createSpyObj

¥Using jasmine.createSpy and jasmine.createSpyObj

jasmine.createSpyObj 从头开始创建一个完整的模拟对象,并在创建时定义了一组模拟方法。这是有用的,因为它非常简单。无需构建任何内容或将任何内容注入到测试中。使用此函数的缺点是它允许创建可能与真实对象不匹配的对象。

¥jasmine.createSpyObj creates a full mock object from scratch with a set of mock methods defined on creation. This is useful in that it is very simple. Nothing needs to be constructed or injected into the test. The disadvantage of using this function is that it allows the creation of objects that may not match the real objects.

jasmine.createSpy 类似,但它创建了一个独立的模拟函数。

¥jasmine.createSpy is similar but it creates a stand-alone mock function.

使用 spyOn()spyOnProperty()

¥Using spyOn() and spyOnProperty()

spyOn() 在现有对象上安装间谍程序。使用此技术的优点是,如果尝试监视对象上不存在的方法,则会引发异常。这可以防止测试模拟不存在的方法。缺点是测试需要一个完全成形的对象来开始,这可能会增加所需的测试设置量。

¥spyOn() installs the spy on an existing object. The advantage of using this technique is that if an attempt is made to spy on a method that does not exist on the object, an exception is raised. This prevents the test from mocking methods that do not exist. The disadvantage is that the test needs a fully formed object to begin with, which may increase the amount of test setup required.

spyOnProperty() 类似,不同之处在于它监视的是属性而不是方法。

¥spyOnProperty() is similar with the difference being that it spies on a property and not a method.

通用测试结构

¥General Testing Structure

单元测试包含在 spec 文件中,每个实体(组件、页面、服务、管道等)有一个 spec 文件。spec 文件与它们正在测试的源并存并以其命名。例如,如果项目有一个名为 WeatherService 的服务,则其代码位于名为 weather.service.ts 的文件中,测试位于名为 weather.service.spec.ts 的文件中。这两个文件位于同一文件夹中。

¥Unit tests are contained in spec files with one spec file per entity (component, page, service, pipe, etc.). The spec files live side-by-side with and are named after the source that they are testing. For example, if the project has a service called WeatherService, the code for it is in a file named weather.service.ts with the tests in a file named weather.service.spec.ts. Both of those files are in the same folder.

spec 文件本身包含一个定义整个测试的 describe 调用。其中嵌套的是定义主要功能区域的其他 describe 调用。每个 describe 调用可以包含设置和拆卸代码(通常通过 beforeEachafterEach 调用处理),更多 describe 调用形成功能的分层分解,以及定义单独测试用例的 it 调用。

¥The spec files themselves contain a single describe call that defines that overall test. Nested within it are other describe calls that define major areas of functionality. Each describe call can contain setup and teardown code (generally handled via beforeEach and afterEach calls), more describe calls forming a hierarchical breakdown of functionality, and it calls which define individual test cases.

describeit 调用还包含描述性文本标签。在格式良好的测试中,describeit 调用与其标签相结合来执行正确的短语,并且每个测试用例的完整标签(通过组合 describeit 标签形成)创建一个完整的句子。

¥The describe and it calls also contain a descriptive text label. In well-formed tests, the describe and it calls combine with their labels to perform proper phrases and the full label for each test case, formed by combining the describe and it labels, creates a full sentence.

例如:

¥For example:

describe('Calculation', () => {
describe('divide', () => {
it('calculates 4 / 2 properly' () => {});
it('cowardly refuses to divide by zero' () => {});
...
});

describe('multiply', () => {
...
});
});

外部 describe 调用说明正在测试 Calculation 服务,内部 describe 调用说明正在测试什么功能,而 it 调用说明测试用例是什么。运行时,每个测试用例的完整标签是一个有意义的句子(计算除法懦弱地拒绝除以零)。

¥The outer describe call states that the Calculation service is being tested, the inner describe calls state exactly what functionality is being tested, and the it calls state what the test cases are. When run the full label for each test case is a sentence that makes sense (Calculation divide cowardly refuses to divide by zero).

页面和组件

¥Pages and Components

页面只是 Angular 组件。因此,页面和组件都使用 Angular 的组件测试 指南进行测试。

¥Pages are just Angular components. Thus, pages and components are both tested using Angular's Component Testing guidelines.

由于页面和组件同时包含 TypeScript 代码和 HTML 模板标记,因此可以执行组件类测试和组件 DOM 测试。创建页面时,生成的模板测试如下所示:

¥Since pages and components contain both TypeScript code and HTML template markup it is possible to perform both component class testing and component DOM testing. When a page is created, the template test that is generated looks like this:



import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';




import { ComponentFixture, TestBed } from '@angular/core/testing';



import { TabsPage } from './tabs.page';

describe('TabsPage', () => {
let component: TabsPage;
let fixture: ComponentFixture

<TabsPage>

;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TabsPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();

fixture = TestBed.createComponent(TabsPage);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

在进行组件类测试时,使用通过 component = fixture.componentInstance;.X 定义的组件对象来访问组件对象。这是组件类的一个实例。在进行 DOM 测试时,会使用 fixture.nativeElement 属性。这是组件的实际 HTMLElement,它允许测试使用标准 HTML API 方法(例如 HTMLElement.querySelector)来检查 DOM。

¥When doing component class testing, the component object is accessed using the component object defined via component = fixture.componentInstance;. This is an instance of the component class. When doing DOM testing, the fixture.nativeElement property is used. This is the actual HTMLElement for the component, which allows the test to use standard HTML API methods such as HTMLElement.querySelector in order to examine the DOM.

服务

¥Services

服务通常属于两大类之一:执行计算和其他操作的实用服务,以及主要执行 HTTP 操作和数据操作的数据服务。

¥Services often fall into one of two broad categories: utility services that perform calculations and other operations, and data services that perform primarily HTTP operations and data manipulation.

基础服务测试

¥Basic Service Testing

测试大多数服务的建议方法是实例化服务并为服务具有的任何依赖手动注入模拟。这样,就可以单独测试代码。

¥The suggested way to test most services is to instantiate the service and manually inject mocks for any dependency the service has. This way, the code can be tested in isolation.

假设有一个服务,其方法采用一系列考勤卡并计算净工资。我们还假设税收计算是通过当前服务所依赖的另一个服务来处理的。该工资单服务可以这样测试:

¥Let's say that there is a service with a method that takes an array of timecards and calculates net pay. Let's also assume that the tax calculations are handled via another service that the current service depends on. This payroll service could be tested as such:

import { PayrollService } from './payroll.service';

describe('PayrollService', () => {
let service: PayrollService;
let taxServiceSpy;

beforeEach(() => {
taxServiceSpy = jasmine.createSpyObj('TaxService', {
federalIncomeTax: 0,
stateIncomeTax: 0,
socialSecurity: 0,
medicare: 0
});
service = new PayrollService(taxServiceSpy);
});

describe('net pay calculations', () => {
...
});
});

这允许测试通过模拟设置(例如 taxServiceSpy.federalIncomeTax.and.returnValue(73.24))控制各种税收计算返回的值。这使得 "净工资" 测试能够独立于税收计算逻辑。当税务代码更改时,只需更改与税务服务相关的代码和测试。净工资测试可以继续按原样运行,因为这些测试不关心税费如何计算,只关心该值是否正确应用。

¥This allows the test to control the values returned by the various tax calculations via mock setup such as taxServiceSpy.federalIncomeTax.and.returnValue(73.24). This allows the "net pay" tests to be independent of the tax calculation logic. When the tax codes change, only the tax service related code and tests need to change. The tests for the net pay can continue to operate as they are since these tests do not care how the tax is calculated, just that the value is applied properly.

通过 ionic g service name 生成服务时使用的脚手架使用 Angular 的测试实用程序并设置测试模块。这样做并不是绝对必要的。但是,可以保留该代码,从而允许手动构建服务或这样注入:

¥The scaffolding that is used when a service is generated via ionic g service name uses Angular's testing utilities and sets up a testing module. Doing so is not strictly necessary. That code may be left in, however, allowing the service to be built manually or injected as such:



import { TestBed, inject } from '@angular/core/testing';



import { PayrollService } from './payroll.service';
import { TaxService } from './tax.service';

describe('PayrolService', () => {
let taxServiceSpy;

beforeEach(() => {
taxServiceSpy = jasmine.createSpyObj('TaxService', {
federalIncomeTax: 0,
stateIncomeTax: 0,
socialSecurity: 0,
medicare: 0,
});
TestBed.configureTestingModule({
providers: [PayrollService, { provide: TaxService, useValue: taxServiceSpy }],
});
});

it('does some test where it is injected', inject([PayrollService], (service: PayrollService) => {
expect(service).toBeTruthy();
}));

it('does some test where it is manually built', () => {
const service = new PayrollService(taxServiceSpy);
expect(service).toBeTruthy();
});
});

测试 HTTP 数据服务

¥Testing HTTP Data Services

大多数执行 HTTP 操作的服务将使用 Angular 的 HttpClient 服务来执行这些操作。对于这样的测试,建议使用 Angular 的 HttpClientTestingModule。有关该模块的详细文档,请参阅 Angular 的 Angular 的测试 HTTP 请求 指南。

¥Most services that perform HTTP operations will use Angular's HttpClient service in order to perform those operations. For such tests, it is suggested to use Angular's HttpClientTestingModule. For detailed documentation of this module, please see Angular's Angular's Testing HTTP requests guide.

此类测试的基本设置如下所示:

¥This basic setup for such a test looks like this:



import { HttpBackend, HttpClient } from '@angular/common/http';




import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';




import { TestBed, inject } from '@angular/core/testing';



import { IssTrackingDataService } from './iss-tracking-data.service';

describe('IssTrackingDataService', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let issTrackingDataService: IssTrackingDataService;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [IssTrackingDataService],
});

httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
issTrackingDataService = new IssTrackingDataService(httpClient);
});

it('exists', inject([IssTrackingDataService], (service: IssTrackingDataService) => {
expect(service).toBeTruthy();
}));

describe('location', () => {
it('gets the location of the ISS now', () => {
issTrackingDataService.location().subscribe((x) => {
expect(x).toEqual({ longitude: -138.1719, latitude: 44.4423 });
});
const req = httpTestingController.expectOne('http://api.open-notify.org/iss-now.json');
expect(req.request.method).toEqual('GET');
req.flush({
iss_position: { longitude: '-138.1719', latitude: '44.4423' },
timestamp: 1525950644,
message: 'success',
});
httpTestingController.verify();
});
});
});

管道

¥Pipes

管道就像具有专门定义的接口的服务。它是一个包含一个公共方法 transform 的类,该方法操作输入值(和其他可选参数)以创建在页面上呈现的输出。测试管道:实例化管道,调用转换方法,并验证结果。

¥A pipe is like a service with a specifically defined interface. It is a class that contains one public method, transform, which manipulates the input value (and other optional arguments) in order to create the output that is rendered on the page. To test a pipe: instantiate the pipe, call the transform method, and verify the results.

作为一个简单的示例,让我们看一下一个采用 Person 对象并格式化名称的管道。为了简单起见,假设 PersonidfirstNamelastNamemiddleInitial 组成。管道的要求是将名称打印为 "最后,第一个 M。",处理名字、姓氏或中间名首字母不存在的情况。这样的测试可能如下所示:

¥As a simple example, let's look at a pipe that takes a Person object and formats the name. For the sake of simplicity, let's say a Person consists of an id, firstName, lastName, and middleInitial. The requirements for the pipe are to print the name as "Last, First M." handling situations where a first name, last name, or middle initial do not exist. Such a test might look like this:

import { NamePipe } from './name.pipe';

import { Person } from '../../models/person';

describe('NamePipe', () => {
let pipe: NamePipe;
let testPerson: Person;

beforeEach(() => {
pipe = new NamePipe();
testPerson = {
id: 42,
firstName: 'Douglas',
lastName: 'Adams',
middleInitial: 'N',
};
});

it('exists', () => {
expect(pipe).toBeTruthy();
});

it('formats a full name properly', () => {
expect(pipe.transform(testPerson)).toBeEqual('Adams, Douglas N.');
});

it('handles having no middle initial', () => {
delete testPerson.middleInitial;
expect(pipe.transform(testPerson)).toBeEqual('Adams, Douglas');
});

it('handles having no first name', () => {
delete testPerson.firstName;
expect(pipe.transform(testPerson)).toBeEqual('Adams N.');
});

it('handles having no last name', () => {
delete testPerson.lastName;
expect(pipe.transform(testPerson)).toBeEqual('Douglas N.');
});
});

通过在使用管道的组件和页面中进行 DOM 测试来运用管道也是有益的。

¥It is also beneficial to exercise the pipe via DOM testing in the components and pages that utilize the pipe.

端到端测试

¥End-to-end Testing

端到端测试用于验证应用作为一个整体是否正常工作,并且通常包括与实时数据的连接。单元测试侧重于孤立的代码单元,因此允许对应用逻辑进行底层测试,而端到端测试则侧重于各种用户故事或使用场景,通过整个数据流提供对整个数据流的高级测试。 应用。单元测试试图发现应用逻辑的问题,而端到端测试则试图发现当这些单独的单元一起使用时出现的问题。端到端测试揭示了应用整体架构的问题。

¥End-to-end testing is used to verify that an application works as a whole and often includes a connection to live data. Whereas unit tests focus on code units in isolation and thus allow for low-level testing of the application logic, end-to-end tests focus on various user stories or usage scenarios, providing high-level testing of the overall flow of data through the application. Whereas unit tests try to uncover problems with an application's logic, end-to-end tests try to uncover problems that occur when those individual units are used together. End-to-end tests uncover problems with the overall architecture of the application.

由于端到端测试练习用户故事并覆盖整个应用而不是单个代码模块,因此除了主应用本身的代码之外,端到端测试还存在于项目中自己的应用中。大多数端到端测试的运行方式是自动执行常见的用户与应用的交互,并检查 DOM 以确定这些交互的结果。

¥Since end-to-end tests exercise user stories and cover the application as a whole rather than individual code modules, end-to-end tests exist in their own application in the project apart from the code for the main application itself. Most end-to-end tests operate by automating common user interactions with the application and examining the DOM to determine the results of those interactions.

测试结构

¥Test Structure

当生成 @ionic/angular 应用时,会在 e2e 文件夹中生成默认的端到端测试应用。该应用使用 量角器 控制浏览器,使用 Jasmine 构建和执行测试。该应用最初由四个文件组成:

¥When an @ionic/angular application is generated, a default end-to-end test application is generated in the e2e folder. This application uses Protractor to control the browser and Jasmine to structure and execute the tests. The application initially consists of four files:

  • protractor.conf.js - 量角器配置文件

    ¥protractor.conf.js - the Protractor configuration file

  • tsconfig.e2e.json - 测试应用的特定 TypeScript 配置

    ¥tsconfig.e2e.json - specific TypeScript configuration for the testing application

  • src/app.po.ts - 页面对象,包含导航应用、查询 DOM 中的元素以及操作页面上的元素的方法

    ¥src/app.po.ts - a page object containing methods that navigate the application, query elements in the DOM, and manipulate elements on the page

  • src/app.e2e-spec.ts - 一个测试脚本

    ¥src/app.e2e-spec.ts - a testing script

页面对象

¥Page Objects

端到端测试的运行方式是自动执行用户与应用的常见交互、等待应用响应并检查 DOM 以确定交互结果。这涉及到大量的 DOM 操作和检查。如果这一切都是手动完成的,测试将非常脆弱并且难以阅读和维护。

¥End-to-end tests operate by automating common user interactions with the application, waiting for the application to respond, and examining the DOM to determine the results of the interaction. This involves a lot of DOM manipulation and examination. If this were all done manually, the tests would be very brittle and difficult to read and maintain.

页面对象将单个页面的 HTML 封装在 TypeScript 类中,提供测试脚本用于与应用交互的 API。将 DOM 操作逻辑封装在页面对象中使测试更具可读性并且更容易推断,从而降低了测试的维护成本。创建精心设计的页面对象是创建高质量且可维护的端到端测试的关键。

¥Page objects encapsulate the HTML for a single page in a TypeScript class, providing an API that the test scripts use to interact with the application. The encapsulation of the DOM manipulation logic in page objects makes the tests more readable and far easier to reason about, lowering the maintenance costs of the test. Creating well-crafted page objects is the key to creating high quality and maintainable end-to-end tests.

基本页面对象

¥Base Page Object

许多测试依赖于诸如等待页面可见、在输入中输入文本以及单击按钮等操作。用于执行此操作的方法仅与用于更改适当 DOM 元素的 CSS 选择器保持一致。因此,将此逻辑抽象为可供其他页面对象使用的基类是有意义的。

¥A lot of tests rely on actions such as waiting for a page to be visible, entering text into an input, and clicking a button. The methods used to do this remain consistent with only the CSS selectors used to get the appropriate DOM element changing. Therefore it makes sense to abstract this logic into a base class that can be used by the other page objects.

下面是一个示例,它实现了所有页面对象都需要支持的一些基本方法。

¥Here is an example that implements a few basic methods that all page objects will need to support.

import { browser, by, element, ExpectedConditions } from 'protractor';

export class PageObjectBase {
private path: string;
protected tag: string;

constructor(tag: string, path: string) {
this.tag = tag;
this.path = path;
}

load() {
return browser.get(this.path);
}

rootElement() {
return element(by.css(this.tag));
}

waitUntilInvisible() {
browser.wait(ExpectedConditions.invisibilityOf(this.rootElement()), 3000);
}

waitUntilPresent() {
browser.wait(ExpectedConditions.presenceOf(this.rootElement()), 3000);
}

waitUntilNotPresent() {
browser.wait(ExpectedConditions.not(ExpectedConditions.presenceOf(this.rootElement())), 3000);
}

waitUntilVisible() {
browser.wait(ExpectedConditions.visibilityOf(this.rootElement()), 3000);
}

getTitle() {
return element(by.css(`${this.tag} ion-title`)).getText();
}

protected enterInputText(sel: string, text: string) {
const el = element(by.css(`${this.tag} ${sel}`));
const inp = el.element(by.css('input'));
inp.sendKeys(text);
}

protected enterTextareaText(sel: string, text: string) {
const el = element(by.css(`${this.tag} ${sel}`));
const inp = el.element(by.css('textarea'));
inp.sendKeys(text);
}

protected clickButton(sel: string) {
const el = element(by.css(`${this.tag} ${sel}`));
browser.wait(ExpectedConditions.elementToBeClickable(el));
el.click();
}
}
每页抽象

¥Per-Page Abstractions

应用中的每个页面都有自己的页面对象类,用于抽象该页面上的元素。如果使用基本页面对象类,则创建页面对象主要涉及为特定于该页面的元素创建自定义方法。通常,这些自定义元素利用基类中的方法来执行所需的工作。

¥Each page in the application will have its own page object class that abstracts the elements on that page. If a base page object class is used, creating the page object involves mostly creating custom methods for elements that are specific to that page. Often, these custom elements take advantage of methods in the base class in order to perform the work that is required.

下面是一个简单但典型的登录页面的示例页面对象。请注意,许多方法(例如 enterEMail())调用基类中执行大部分工作的方法。

¥Here is an example page object for a simple but typical login page. Notice that many of the methods, such as enterEMail(), call methods in the base class that perform the bulk of the work.

import { browser, by, element, ExpectedConditions } from 'protractor';
import { PageObjectBase } from './base.po';

export class LoginPage extends PageObjectBase {
constructor() {
super('app-login', '/login');
}

waitForError() {
browser.wait(ExpectedConditions.presenceOf(element(by.css('.error'))), 3000);
}

getErrorMessage() {
return element(by.css('.error')).getText();
}

enterEMail(email: string) {
this.enterInputText('#email-input', email);
}

enterPassword(password: string) {
this.enterInputText('#password-input', password);
}

clickSignIn() {
this.clickButton('#signin-button');
}
}

测试脚本

¥Testing Scripts

与单元测试类似,端到端测试脚本由嵌套的 describe()it() 函数组成。在端到端测试的情况下,describe() 函数通常表示特定场景,而 it() 函数表示应用在该场景中执行操作时应表现出的特定行为。

¥Similar to unit tests, end-to-end test scripts consist of nested describe() and it() functions. In the case of end-to-end tests, the describe() functions generally denote specific scenarios with the it() functions denoting specific behaviors that should be exhibited by the application as actions are performed within that scenario.

与单元测试类似,describe()it() 函数中使用的标签对于 "describe" 或 "it" 以及连接在一起形成完整的测试用例都应该有意义。

¥Also similar to unit tests, the labels used in the describe() and it() functions should make sense both with the "describe" or "it" and when concatenated together to form the complete test case.

这是一个示例端到端测试脚本,用于练习一些典型的登录场景。

¥Here is a sample end-to-end test script that exercises some typical login scenarios.

import { AppPage } from '../page-objects/pages/app.po';
import { AboutPage } from '../page-objects/pages/about.po';
import { CustomersPage } from '../page-objects/pages/customers.po';
import { LoginPage } from '../page-objects/pages/login.po';
import { MenuPage } from '../page-objects/pages/menu.po';
import { TasksPage } from '../page-objects/pages/tasks.po';

describe('Login', () => {
const about = new AboutPage();
const app = new AppPage();
const customers = new CustomersPage();
const login = new LoginPage();
const menu = new MenuPage();
const tasks = new TasksPage();

beforeEach(() => {
app.load();
});

describe('before logged in', () => {
it('displays the login screen', () => {
expect(login.rootElement().isDisplayed()).toEqual(true);
});

it('allows in-app navigation to about', () => {
menu.clickAbout();
about.waitUntilVisible();
login.waitUntilInvisible();
});

it('does not allow in-app navigation to tasks', () => {
menu.clickTasks();
app.waitForPageNavigation();
expect(login.rootElement().isDisplayed()).toEqual(true);
});

it('does not allow in-app navigation to customers', () => {
menu.clickCustomers();
app.waitForPageNavigation();
expect(login.rootElement().isDisplayed()).toEqual(true);
});

it('displays an error message if the login fails', () => {
login.enterEMail('test@test.com');
login.enterPassword('bogus');
login.clickSignIn();
login.waitForError();
expect(login.getErrorMessage()).toEqual('The password is invalid or the user does not have a password.');
});

it('navigates to the tasks page if the login succeeds', () => {
login.enterEMail('test@test.com');
login.enterPassword('testtest');
login.clickSignIn();
tasks.waitUntilVisible();
});
});

describe('once logged in', () => {
beforeEach(() => {
tasks.waitUntilVisible();
});

it('allows navigation to the customers page', () => {
menu.clickCustomers();
customers.waitUntilVisible();
tasks.waitUntilInvisible();
});

it('allows navigation to the about page', () => {
menu.clickAbout();
about.waitUntilVisible();
tasks.waitUntilInvisible();
});

it('allows navigation back to the tasks page', () => {
menu.clickAbout();
tasks.waitUntilInvisible();
menu.clickTasks();
tasks.waitUntilVisible();
});
});
});

配置

¥Configuration

默认配置使用与开发相同的 environment.ts 文件。为了更好地控制端到端测试所使用的数据,创建特定的测试环境并使用该环境进行测试通常很有用。本节展示创建此配置的一种可能方法。

¥The default configuration uses the same environment.ts file that is used for development. In order to provide better control over the data used by the end-to-end tests, it is often useful to create a specific environment for testing and use that environment for the tests. This section shows one possible way to create this configuration.

测试环境

¥Testing Environment

设置测试环境包括创建一个使用专用测试后端的新环境文件、更新 angular.json 文件以使用该环境,以及修改 package.json 中的 e2e 脚本以指定 test 环境。

¥Setting up a testing environment involves creating a new environment file that uses a dedicated testing backend, updating the angular.json file to use that environment, and modifying the e2e script in the package.json to specify the test environment.

创建 environment.e2e.ts 文件

¥Create the environment.e2e.ts File

Angular environment.tsenvironment.prod.ts 文件通常用于存储应用后端数据服务的基本 URL 等信息。创建一个提供相同信息的 environment.e2e.ts,仅连接到专用于测试的后端服务,而不是连接到开发或生产后端服务。这是一个例子:

¥The Angular environment.ts and environment.prod.ts files are often used to store information such as the base URL for the application's backend data services. Create an environment.e2e.ts that provides the same information, only connecting to backend services that are dedicated to testing rather than the development or production backend services. Here is an example:

export const environment = {
production: false,
databaseURL: 'https://e2e-test-api.my-great-app.com',
projectId: 'my-great-app-e2e',
};
修改 angular.json 文件

¥Modify the angular.json File

需要修改 angular.json 文件才能使用该文件。这是一个分层的过程。按照下面列出的 XPath 添加所需的配置。

¥The angular.json file needs to be modified to use this file. This is a layered process. Follow the XPaths listed below to add the configuration that is required.

/projects/app/architect/build/configurations 添加一个名为 test 的配置来执行文件替换:

¥Add a configuration at /projects/app/architect/build/configurations called test that does the file replacement:

"test": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.e2e.ts"
}
]
}

/projects/app/architect/serve/configurations 添加一个名为 test 的配置,该配置将浏览器目标指向上面定义的 test 构建配置。

¥Add a configuration at /projects/app/architect/serve/configurations called test that points the browser target at the test build configuration that was defined above.

"test": {
"browserTarget": "app:build:test"
}

/projects/app-e2e/architect/e2e/configurations 添加一个名为 test 的配置,该配置将开发服务器目标指向上面定义的 test 服务配置。

¥Add a configuration at /projects/app-e2e/architect/e2e/configurations called test that does points the dev server target at the test serve configuration defined above.

"test": {
"devServerTarget": "app:serve:test"
}
修改 package.json 文件

¥Modify the package.json File

修改 package.json 文件,使 npm run e2e 使用 test 配置。

¥Modify the package.json file so that npm run e2e uses the test configuration.

"scripts": {
"e2e": "ng e2e --configuration=test",
"lint": "ng lint",
"ng": "ng",
"start": "ng serve",
"test": "ng test",
"test:dev": "ng test --browsers=ChromeHeadlessCI",
"test:ci": "ng test --no-watch --browsers=ChromeHeadlessCI"
},

测试清理

¥Test Cleanup

如果端到端测试以任何方式修改数据,则在测试完成后将数据重置为已知状态会很有帮助。一种方法是:

¥If the end-to-end tests modify data in any way it is helpful to reset the data to a known state once the test completes. One way to do that is to:

  1. 创建执行清理的端点。

    ¥Create an endpoint that performs the cleanup.

  2. onCleanUp() 函数添加到 protractor.conf.js 文件导出的 config 对象中。

    ¥Add a onCleanUp() function to the config object exported by the protractor.conf.js file.

这是一个例子:

¥Here is an example:

onCleanUp() {
const axios = require('axios');
return axios
.post(
'https://e2e-test-api.my-great-app.com/purgeDatabase',
{}
)
.then(res => {
console.log(res.data);
})
.catch(err => console.log(err));
}