千家信息网

Angular单元测试与E2E测试

发表于:2025-01-23 作者:千家信息网编辑
千家信息网最后更新 2025年01月23日,本文介绍了Angular 7单元测试和E2E测试的配置与测试方法。使用Angular CLI新建工程,已配置好基础测试环境,生成了测试样例代码。默认,Angular单元测试使用Jasmine测试框架和
千家信息网最后更新 2025年01月23日Angular单元测试与E2E测试

本文介绍了Angular 7单元测试和E2E测试的配置与测试方法。使用Angular CLI新建工程,已配置好基础测试环境,生成了测试样例代码。默认,Angular单元测试使用Jasmine测试框架和Karma测试运行器,E2E测试使用Jasmine和Protractor测试框架。

配置单元测试

Jasmine是用于测试JavaScript的行为驱动(Behavior-Driven)框架,不依赖于任何其他JavaScript框架。
Karma是测试运行器,为开发人员提供了高效、真实的测试环境,支持多种浏览器,易于调试。

配置文件

单元测试配置文件test.ts和karma.conf.js:
test.ts

import 'zone.js/dist/zone-testing';import { getTestBed } from '@angular/core/testing';import {  BrowserDynamicTestingModule,  platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';declare const require: any;// First, initialize the Angular testing environment.getTestBed().initTestEnvironment(  BrowserDynamicTestingModule,  platformBrowserDynamicTesting());// Then we find all the tests.const context = require.context('./', true, /\.spec\.ts$/);// And load the modules.context.keys().map(context);

默认,测试文件扩展名必须为.spec.ts。
karma.conf.js

module.exports = function (config) {  config.set({    basePath: '',    frameworks: ['jasmine', '@angular-devkit/build-angular'],    plug×××: [      require('karma-jasmine'),      require('karma-chrome-launcher'),      require('karma-jasmine-html-reporter'),      require('karma-coverage-istanbul-reporter'),      require('@angular-devkit/build-angular/plug×××/karma')    ],    client: {      clearContext: false // leave Jasmine Spec Runner output visible in browser    },    coverageIstanbulReporter: {      dir: require('path').join(__dirname, '../coverage'),      reports: ['html', 'lcovonly'],      fixWebpackSourcePaths: true    },    reporters: ['progress', 'kjhtml'],    port: 9876,    colors: true,    logLevel: config.LOG_INFO,    autoWatch: true,    browsers: ['Chrome'],    singleRun: false  });};

默认,使用Chrome浏览器,可生成单元测试报告和覆盖率报告,覆盖率报告保存在根目录coverage文件夹内,启用autoWatch。
singleRun默认为false,如设为true则测试结束后会自动退出并根据测试结果返回代码0或1,常用于CI环境。

浏览器配置

Karma支持的浏览器:

  • Chrome
  • ChromeCanary
  • ChromeHeadless
  • PhantomJS
  • Firefox
  • Opera
  • IE
  • Safari

可同时配置多个浏览器进行测试,要启用其他浏览器,需安装依赖,比如启用Firefox:

npm i karma-firefox-launcher --save-dev

然后在karma.conf.js内增加配置:

...require('karma-chrome-launcher'),require('karma-firefox-launcher'),...browsers: ['Chrome', 'Firefox'],...

运行测试

用CLI创建App生成了一个单元测试文件app.component.spec.ts。执行CLI命令ng test即可运行单元测试:

ng test

运行后在控制台输出测试结果并打开浏览器:

浏览器会显示测试结果,总测试数,失败数。在顶部,每个点或叉对应一个测试用例,点表示成功,叉表示失败,鼠标移到点或叉上会显示测试信息。点击测试结果中的某一行,可重新运行某个或某组(测试套件)测试。

常用参数:
--browsers 指定使用的浏览器
--code-coverage 输出覆盖率报告
--code-coverage-exclude 排除文件或路径
--karma-config 指定Karma配置文件
--prod 启用production环境
--progress 默认为true,将编译进度输出到控制台
--watch 默认为true,代码修改后会重新运行测试

自定义Launcher

karma-chrome-launcher、karma-firefox-launcher、karma-ie-launcher等均支持自定义Launcher,customLaunchers与--browsers结合使用可满足多种环境的测试需求。每种浏览器支持的自定义属性请查看Karma Browsers文档。
比如,CI环境下常用Headless模式,不需显示浏览器界面,在karma.conf.js中增加如下配置:

browsers: ['Chrome'],customLaunchers: {  ChromeHeadlessCI: {    base: 'ChromeHeadless',    flags: ['--no-sandbox']  }},

运行如下命令进行测试:

ng test --watch=false --progress=false --browsers=ChromeHeadlessCI

测试覆盖率

运行如下命令生成测试覆盖率报告,报告保存在项目根目录下的coverage文件夹内:

ng test --watch=false --code-coverage

如想每次测试都生成报告,可修改CLI配置文件angular.json:

"test": {  "options": {    "codeCoverage": true  }}

设置排除的文件或路径

ng test --watch=false --code-coverage --code-coverage-exclude=src/app/heroes/heroes.component.ts --code-coverage-exclude=src/app/hero-search/*

同样可以在angular.json中配置:

"test": {  "options": {    "codeCoverage": true,    "codeCoverageExclude": ["src/app/heroes/heroes.component.ts", "src/app/hero-search/*"]  }}

设定测试覆盖率指标
编辑配置文件karma.conf.js,增加如下内容:

coverageIstanbulReporter: {  reports: [ 'html', 'lcovonly' ],  fixWebpackSourcePaths: true,  thresholds: {    statements: 80,    lines: 80,    branches: 80,    functions: 80  }}

测试报告中达到标准的背景为绿色:

注意:与CI集成时不要设置覆盖率指标,否则若未到达指标,Job会终止。
LCOV
coverageIstanbulReporter中reports参数为[ 'html', 'lcovonly' ],会生成html和lcov两种格式的报告。报告文件lcov.info可与Sonar集成,在Sonar管理界面配置LCOV Files路径,即可在Sonar中查看测试情况。

另外,与Sonar集成时需配置TypeScript Exclusions,排除.spec.ts,否则统计覆盖率时将包含测试文件。

编写测试

第一个测试

使用CLI创建Service、Component等时会自动创建测试文件,我们以创建App时生成的测试文件app.component.spec.ts为例:

import {async, TestBed} from '@angular/core/testing';import {RouterTestingModule} from '@angular/router/testing';import {AppComponent} from './app.component';describe('AppComponent', () => {  beforeEach(async(() => {    TestBed.configureTestingModule({      imports: [        RouterTestingModule      ],      declarations: [        AppComponent      ],    }).compileComponents();  }));  it('should create the app', () => {    const fixture = TestBed.createComponent(AppComponent);    const app = fixture.debugElement.componentInstance;    expect(app).toBeTruthy();  });  it(`should have as title 'hello'`, () => {    const fixture = TestBed.createComponent(AppComponent);    const app = fixture.debugElement.componentInstance;    expect(app.title).toEqual('hello');  });  it('should render title in a h2 tag', () => {    const fixture = TestBed.createComponent(AppComponent);    fixture.detectChanges();    const compiled = fixture.debugElement.nativeElement;    expect(compiled.querySelector('h2').textContent).toContain('Welcome to hello!');  });});

测试结构
从上例我们可以了解测试的主要结构:
describe函数中包含了beforeEach和it两类函数。describe相当于Java测试中的suite,也就是测试组,其中可以包含多个测试用例it。一般一个测试文件含有一个describe,当然也可以有多个。beforeEach相当于Java测试中的@Before方法,每个测试用例执行前调用一次。同样,还有afterEach、beforeAll、afterAll函数,afterEach在每个测试用例执行后调用一次,beforeAll、afterAll相当于Java测试中的@BeforeClass、@AfterClass方法,每个describe执行前后调用一次。

describe和it的第一个参数是测试说明。一个it中可以包含一个或多个expect来执行测试验证。

TestBed
TestBed是Angular测试中最重要的工具。

TestBed.configureTestingModule()方法动态构建TestingModule来模拟Angular @NgModule,支持@NgModule的大多数属性。

测试中需导入测试的组件及依赖。在AppComponent页面中使用了router-outlet,因此我们导入了RouterTestingModule来模拟RouterModule。Test Module预配置了一些元素,比如BrowserModule,不需导入。

TestBed.createComponent()方法创建组件实例,返回ComponentFixture。ComponentFixture是一个测试工具(test harness),用于与创建的组件和相应元素进行交互。

nativeElement和DebugElement
示例中使用了fixture.debugElement.nativeElement,也可以写成fixture.nativeElement。实际上,fixture.nativeElement是fixture.debugElement.nativeElement的一种简化写法。nativeElement依赖于运行时环境,Angular依赖DebugElement抽象来支持跨平台。Angular创建DebugElement tree来包装native element,nativeElement返回平台相关的元素对象。

我们的测试样例仅运行在浏览器中,因此nativeElement总为HTMLElement,可以使用querySelector()、querySelectorAll()方法来查询元素。

element.querySelector('p');element.querySelector('input');element.querySelector('.welcome');element.querySelectorAll('span');

detectChanges
createComponent() 函数不会绑定数据,必须调用fixture.detectChanges()来执行数据绑定,才能在组件元素中取得内容:

it('should render title in a h2 tag', () => {  const fixture = TestBed.createComponent(AppComponent);  fixture.detectChanges();  const compiled = fixture.debugElement.nativeElement;  expect(compiled.querySelector('h2').textContent).toContain('Welcome to hello!');});

当数据模型值改变后,也需调用fixture.detectChanges()方法:

it('should render title in a h2 tag', () => {  const fixture = TestBed.createComponent(AppComponent);  const app = fixture.componentInstance;  app.title = 'china';  fixture.detectChanges();  const compiled = fixture.nativeElement;  expect(compiled.querySelector('h2').textContent).toContain('Welcome to china!');});

可以配置自动检测,增加ComponentFixtureAutoDetect provider:

import { ComponentFixtureAutoDetect } from '@angular/core/testing';...TestBed.configureTestingModule({  providers: [    { provide: ComponentFixtureAutoDetect, useValue: true }  ]});

启用自动检测后仅需在数值改变后调用detectChanges():

it('should display original title', () => {  // Hooray! No `fixture.detectChanges()` needed  expect(h2.textContent).toContain(comp.title);});it('should still see original title after comp.title change', () => {  const oldTitle = comp.title;  comp.title = 'Test Title';  // Displayed title is old because Angular didn't hear the change :(  expect(h2.textContent).toContain(oldTitle);});it('should display updated title after detectChanges', () => {  comp.title = 'Test Title';  fixture.detectChanges(); // detect changes explicitly  expect(h2.textContent).toContain(comp.title);});

同步和异步beforeEach
组件常用 @Component.templateUrl 和 @Component.styleUrls 属性来指定外部模板和CSS,Angular编译器会在编译期间读取外部文件。

@Component({  selector: 'app-banner',  templateUrl: './banner-external.component.html',  styleUrls:  ['./banner-external.component.css']})
beforeEach(() => {  TestBed.configureTestingModule({    declarations: [ BannerComponent ],  });  fixture = TestBed.createComponent(BannerComponent);});

当用CLI的ng test命令运行含有如上同步beforeEach方法的测试时没有问题,因为会在运行测试之前先编译。若在非CLI环境下运行这些测试则可能失败。要解决这个问题,可以调用compileComponents()进行显示的编译。compileComponents()方法是异步的,必须在async()方法中调用:

beforeEach(async(() => {  TestBed.configureTestingModule({    imports: [      RouterTestingModule    ],    declarations: [      AppComponent    ],  }).compileComponents();}));

调用compileComponents()会关闭当前的TestBed实例,不再允许进行配置,不能再调用任何TestBed中的配置方法,既不能调 configureTestingModule(),也不能调用任何 override... 方法。

常同时使用同步beforeEach和异步beforeEach来协同工作,异步的 beforeEach() 负责编译组件,同步的beforeEach()负责执行其余的准备代码。测试运行器会先调用异步beforeEach方法,运行完毕后再调用同步方法。

重构
示例中重复代码较多,我们用两个beforeEach来简化一下:

import {async, ComponentFixture, TestBed} from '@angular/core/testing';import {RouterTestingModule} from '@angular/router/testing';import {AppComponent} from './app.component';describe('AppComponent', () => {  let fixture: ComponentFixture;  let app: AppComponent;  beforeEach(async(() => {    TestBed.configureTestingModule({      imports: [        RouterTestingModule      ],      declarations: [        AppComponent      ],    }).compileComponents();  }));  beforeEach(() => {    fixture = TestBed.createComponent(AppComponent);    app = fixture.componentInstance;    fixture.detectChanges();  });  it('should create the app', () => {    expect(app).toBeTruthy();  });  it(`should have as title 'hello'`, () => {    expect(app.title).toEqual('hello');  });  it('should render title in a h2 tag', () => {    const compiled = fixture.nativeElement;    expect(compiled.querySelector('h2').textContent).toContain('Welcome to hello!');  });});

也可以把这两个 beforeEach() 重构成一个异步的beforeEach():

beforeEach(async(() => {  TestBed.configureTestingModule({     imports: [        RouterTestingModule      ],      declarations: [        AppComponent      ],  })  .compileComponents()  .then(() => {    fixture = TestBed.createComponent(AppComponent);    app = fixture.componentInstance;    fixture.detectChanges();  });}));

依赖注入与Mock

对简单对象进行测试可以用new创建实例:

describe('ValueService', () => {  let service: ValueService;  beforeEach(() => { service = new ValueService(); });    ...});

不过大多数Service、Component等有多个依赖项,使用new很不方便。若用DI来创建测试对象,当依赖其他服务时,DI会找到或创建依赖的服务。要测试某个对象,在configureTestingModule中配置测试对象本身及依赖项,然后调用TestBed.get()注入测试对象:

beforeEach(() => {  TestBed.configureTestingModule({ providers: [ValueService] });  service = TestBed.get(ValueService);});

单元测试的原则之一:仅对要测试对象本身进行测试,而不对其依赖项进行测试,依赖项通过mock方式注入,而不使用实际的对象,否则测试不可控。

Mock优先使用Spy方式:

let masterService: MasterService;beforeEach(() => {  const spy = jasmine.createSpyObj('ValueService', ['getValue']);    spy.getValue.and.returnValue('stub value');  TestBed.configureTestingModule({    // Provide both the service-to-test and its (spy) dependency    providers: [      MasterService,      { provide: ValueService, useValue: spy }    ]  });  masterService = TestBed.get(MasterService);});

HttpClient、Router、Location

同测试含其它依赖的对象一样,可以mock HttpClient、Router、Location:

beforeEach(() => {  const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);  TestBed.configureTestingModule({    providers: [      {provide: HttpClient, useValue: httpClientSpy}    ]  });});
beforeEach(async(() => {  const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);  const locationSpy = jasmine.createSpyObj('Location', ['back']);  TestBed.configureTestingModule({    providers: [      {provide: Router, useValue: routerSpy},      {provide: Location, useValue: locationSpy}    ]  })    .compileComponents();}));

Component测试

  • 仅测试组件类

测试组件类就像测试服务那样简单:
组件类

export class WelcomeComponent  implements OnInit {  welcome: string;  constructor(private userService: UserService) { }  ngOnInit(): void {    this.welcome = this.userService.isLoggedIn ?      'Welcome, ' + this.userService.user.name : 'Please log in.';  }}

Mock类

class MockUserService {  isLoggedIn = true;  user = { name: 'Test User'};};

测试

...beforeEach(() => {  TestBed.configureTestingModule({    // provide the component-under-test and dependent service    providers: [      WelcomeComponent,      { provide: UserService, useClass: MockUserService }    ]  });  // inject both the component and the dependent service.  comp = TestBed.get(WelcomeComponent);  userService = TestBed.get(UserService);});...it('should ask user to log in if not logged in after ngOnInit', () => {  userService.isLoggedIn = false;  comp.ngOnInit();  expect(comp.welcome).not.toContain(userService.user.name);  expect(comp.welcome).toContain('log in');});
  • 组件DOM测试

只涉及类的测试可以判断组件类的行为是否正常,但不能确定组件是否能正常渲染和交互。
进行组件DOM测试,需要使用TestBed.createComponent()等方法,第一个测试即为组件DOM测试。

TestBed.configureTestingModule({  declarations: [ BannerComponent ]});const fixture = TestBed.createComponent(BannerComponent);const component = fixture.componentInstance;expect(component).toBeDefined();

dispatchEvent
为模拟用户输入,比如为input元素输入值,要找到input元素并设置它的 value 属性。Angular不知道你设置了input元素的value属性,需要调用 dispatchEvent() 触发输入框的 input 事件,再调用 detectChanges():

it('should convert hero name to Title Case', () => {  // get the name's input and display elements from the DOM  const hostElement = fixture.nativeElement;  const nameInput: HTMLInputElement = hostElement.querySelector('input');  const nameDisplay: HTMLElement = hostElement.querySelector('span');  nameInput.value = 'quick BROWN  fOx';  // dispatch a DOM event so that Angular learns of input value change.  nameInput.dispatchEvent(newEvent('input'));  fixture.detectChanges();  expect(nameDisplay.textContent).toBe('Quick Brown  Fox');});

嵌套组件

组件中常常使用其他组件:

对于无害的内嵌组件可以直接将其添加到declarations中,这是最简单的方式:

describe('AppComponent & TestModule', () => {  beforeEach(async(() => {    TestBed.configureTestingModule({      declarations: [        AppComponent,        BannerComponent,        WelcomeComponent      ]    })    .compileComponents().then(() => {      fixture = TestBed.createComponent(AppComponent);      comp    = fixture.componentInstance;    });  }));  ...});

也可为无关紧要的组件创建一些测试桩:

@Component({selector: 'app-banner', template: ''})class BannerStubComponent {}@Component({selector: 'router-outlet', template: ''})class RouterOutletStubComponent { }@Component({selector: 'app-welcome', template: ''})class WelcomeStubComponent {}

然后在TestBed的配置中声明它们:

TestBed.configureTestingModule({  declarations: [    AppComponent,    BannerStubComponent,    RouterOutletStubComponent,    WelcomeStubComponent  ]})

另一种办法是使用NO_ERRORS_SCHEMA,要求 Angular编译器忽略那些不认识的元素和属性:

TestBed.configureTestingModule({  declarations: [    AppComponent,    RouterLinkDirectiveStub  ],  schemas: [ NO_ERRORS_SCHEMA ]})

NO_ERRORS_SCHEMA方法比较简单,但不要过度使用。NO_ERRORS_SCHEMA 会阻止编译器因疏忽或拼写错误而缺失的组件和属性,如人工找出这些 bug会很费时。
RouterLinkDirectiveStub

import { Directive, Input, HostListener } from '@angular/core';@Directive({  selector: '[routerLink]'})export class RouterLinkDirectiveStub {  @Input('routerLink') linkParams: any;  navigatedTo: any = null;  @HostListener('click')  onClick() {    this.navigatedTo = this.linkParams;  }}

属性指令测试

import { Directive, ElementRef, Input, OnChanges } from '@angular/core';@Directive({ selector: '[highlight]' })/** Set backgroundColor for the attached element to highlight color and set the element's customProperty to true */export class HighlightDirective implements OnChanges {  defaultColor =  'rgb(211, 211, 211)'; // lightgray  @Input('highlight') bgColor: string;  constructor(private el: ElementRef) {    el.nativeElement.style.customProperty = true;  }  ngOnChanges() {    this.el.nativeElement.style.backgroundColor = this.bgColor || this.defaultColor;  }}

属性型指令肯定要操纵 DOM,如只针对类测试不能证明指令的有效性。若通过组件来测试,单一的用例一般无法探索指令的全部能力。因此,更好的方法是创建一个能展示该指令所有用法的人造测试组件:

@Component({  template: `  

Something Yellow

The Default (Gray)

No Highlight

`})class TestComponent { }

测试程序:

beforeEach(() => {  fixture = TestBed.configureTestingModule({    declarations: [ HighlightDirective, TestComponent ]  })  .createComponent(TestComponent);  fixture.detectChanges(); // initial binding  // all elements with an attached HighlightDirective  des = fixture.debugElement.queryAll(By.directive(HighlightDirective));  // the h3 without the HighlightDirective  bareH2 = fixture.debugElement.query(By.css('h3:not([highlight])'));});// color testsit('should have three highlighted elements', () => {  expect(des.length).toBe(3);});it('should color 1st 

background "yellow"', () => { const bgColor = des[0].nativeElement.style.backgroundColor; expect(bgColor).toBe('yellow');});it('should color 2nd

background w/ default color', () => { const dir = des[1].injector.get(HighlightDirective) as HighlightDirective; const bgColor = des[1].nativeElement.style.backgroundColor; expect(bgColor).toBe(dir.defaultColor);});it('should bind background to value color', () => { // easier to work with nativeElement const input = des[2].nativeElement as HTMLInputElement; expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor'); // dispatch a DOM event so that Angular responds to the input value change. input.value = 'green'; input.dispatchEvent(newEvent('input')); fixture.detectChanges(); expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');});it('bare

should not have a customProperty', () => { expect(bareH2.properties['customProperty']).toBeUndefined();});

Pipe测试

describe('TitleCasePipe', () => {  // This pipe is a pure, stateless function so no need for BeforeEach  let pipe = new TitleCasePipe();  it('transforms "abc" to "Abc"', () => {    expect(pipe.transform('abc')).toBe('Abc');  });  it('transforms "abc def" to "Abc Def"', () => {    expect(pipe.transform('abc def')).toBe('Abc Def');  });  ...});

Testing Module

RouterTestingModule
在前面的测试中我们使用了测试桩RouterOutletStubComponent,与Router有关的测试还可以使用RouterTestingModule:

beforeEach(async(() => {  TestBed.configureTestingModule({    imports: [      RouterTestingModule    ],    declarations: [      AppComponent    ],  }).compileComponents();}));

RouterTestingModule还可以模拟路由:

beforeEach(() => {  TestBed.configureTestModule({    imports: [      RouterTestingModule.withRoutes(        [{path: '', component: BlankCmp}, {path: 'simple', component: SimpleCmp}]      )    ]  });});

HttpClientTestingModule

describe('HttpClient testing', () => {  let httpClient: HttpClient;  let httpTestingController: HttpTestingController;  beforeEach(() => {    TestBed.configureTestingModule({      imports: [ HttpClientTestingModule ]    });    // Inject the http service and test controller for each test    httpClient = TestBed.get(HttpClient);    httpTestingController = TestBed.get(HttpTestingController);  });  afterEach(() => {    // After every test, assert that there are no more pending requests.    httpTestingController.verify();  });  it('can test HttpClient.get', () => {    const testData: Data = {name: 'Test Data'};    // Make an HTTP GET request    httpClient.get(testUrl)      .subscribe(data =>        // When observable resolves, result should match test data        expect(data).toEqual(testData)      );    // The following `expectOne()` will match the request's URL.    // If no requests or multiple requests matched that URL    // `expectOne()` would throw.    const req = httpTestingController.expectOne('/data');    // Assert that the request is a GET.    expect(req.request.method).toEqual('GET');    // Respond with mock data, causing Observable to resolve.    // Subscribe callback asserts that correct data was returned.    req.flush(testData);    // Finally, assert that there are no outstanding requests.    httpTestingController.verify();  });    ...});

调试

在浏览器测试结果页面,点击"DEBUG"按钮会打开新浏标签页并重新运行测试程序。按"F12"打开调试界面,然后进入Sources找到测试文件(CTRL+P),在测试程序中设置断点即可调试。

配置E2E测试

E2E测试使用Jasmine和Protractor测试框架,Protractor是Angular端到端测试框架。

安装Protractor

npm i -g protractor

安装后,node_modules\protractor\bin目录含有两个命令行工具protractor和webdriver-manager,其中webdriver-manager负责管理驱动、启停Selenium Server。

webdriver-manager命令:

clean      removes all downloaded driver files from the out_dirstart      start up the selenium servershutdown   shut down the selenium serverstatus     list the current available driversupdate     update selected binariesversion    get the current version

更新驱动:

webdriver-manager update

默认安装chromedriver、geckodriver和selenium standalone,驱动目录为node_modules\protractor\node_modules\webdriver-manager\selenium,下载使用的url配置在webdriver-manager\config.json文件内:

"cdnUrls": {  "selenium": "https://selenium-release.storage.googleapis.com/",  "chromedriver": "https://chromedriver.storage.googleapis.com/",  "geckodriver": "https://github.com/mozilla/geckodriver/releases/download/",  "iedriver": "https://selenium-release.storage.googleapis.com/",  "androidsdk": "http://dl.google.com/android/"}

可以修改为其它CDN:

"cdnUrls": {  "selenium": "https://mirrors.huaweicloud.com/selenium/",  "chromedriver": "https://mirrors.huaweicloud.com/chromedriver/",  "geckodriver": "https://mirrors.huaweicloud.com/geckodriver/",  "iedriver": "https://selenium-release.storage.googleapis.com/",  "androidsdk": "http://dl.google.com/android/"}

也可以使用参数--alternate_cdn:

webdriver-manager update --alternate_cdn=...

配置文件

使用CLI创建的App会生成一个e2e项目,其中包含测试配置protractor.conf.js及测试代码。
protractor.conf.js

const { SpecReporter } = require('jasmine-spec-reporter');exports.config = {  allScriptsTimeout: 11000,  specs: [    './src/**/*.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: require('path').join(__dirname, './tsconfig.e2e.json')    });    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));  }};

默认,Protractor使用Jasmine测试框架,使用直连方式连接Chrome浏览器,测试文件扩展名为.e2e-spec.ts。

浏览器配置

Protractor支持Chrome、Firefox、Safari、IE等浏览器。
多浏览器
Protractor支持同时启动多个浏览器,一个浏览器时,在配置中使用capabilities选项;多个浏览器时,使用multiCapabilities:

multiCapabilities: [{  browserName: 'firefox'}, {  browserName: 'chrome'}]

另外需在package.json中增加配置:

"scripts": {  "webdriver-update": "webdriver-manager update"}

在运行测试前更新浏览器驱动:

npm run webdriver-update

否则项目中的驱动不会更新(默认只有chrome驱动,运行webdriver-manager update仅更新全局的驱动),运行测试会报如下错误:

No update-config.json found. Run 'webdriver-manager update' to download binaries

浏览器选项

capabilities: {  'browserName': 'chrome',  'chromeOptions': {    'args': ['show-fps-counter=true']  }},
capabilities: {  'browserName': 'firefox',  'moz:firefoxOptions': {    'args': ['--safe-mode']  }},

更多选项请查看相应驱动ChromeDriver、GeckoDriver。

Selenium Server配置

使用Standalone Selenium Server时,需安装JDK。
更新driver后启动Selenium Server:

webdriver-manager updatewebdriver-manager start

删除原配置中的directConnect、baseUrl:

directConnect: true,baseUrl: 'http://localhost:4200/',

增加seleniumAddress(默认为http://localhost:4444/wd/hub):

seleniumAddress: 'http://localhost:4444/wd/hub',

运行测试

运行E2E测试:

ng e2e

常用参数:

--base-url  Base URL for protractor to connect to.--configuration (-c)  A named configuration environment, as specified in the "configurations" section of angular.json.--host  Host to listen on.--port  The port to use to serve the application.--prod  When true, sets the build configuration to the production environment.--protractor-config  The name of the Protractor configuration file.--webdriver-update  Try to update webdriver.

driver安装好后,若未更新浏览器,不必每次都更新driver:

ng e2e --webdriver-update=false

如运行测试时报如下错误:

events.js:167      throw er; // Unhandled 'error' event      ^Error: read ECONNRESET    at TLSWrap.onStreamRead (internal/stream_base_commons.js:111:27)Emitted 'error' event

可尝试更新package后再测试:

npm i npm@latest -gnpm update

指定配置文件

不同的环境若配置不同,可使用不同的配置文件。

比如,在CI环境中启用Chrome Headless模式:
在e2e根目录下创建一名为protractor-ci.conf.js的新文件,内容如下:

const config = require('./protractor.conf').config;config.capabilities = {  browserName: 'chrome',  chromeOptions: {    args: ['--headless', '--no-sandbox']  }};exports.config = config;

注意: windows系统要增加参数--disable-gpu

运行以下命令测试:

ng e2e --protractor-config=e2e\protractor-ci.conf.js --webdriver-update=false

编写E2E测试

第一个测试

import { AppPage } from './app.po';describe('workspace-project App', () => {  let page: AppPage;  beforeEach(() => {    page = new AppPage();  });  it('should display welcome message', () => {    page.navigateTo();    expect(page.getTitleText()).toEqual('Welcome to hello!');  });});
import { browser, by, element } from 'protractor';export class AppPage {  navigateTo() {    return browser.get('/');  }  getTitleText() {    return element(by.css('app-root h2')).getText();  }}

E2E测试与单元测试都使用了Jasmine,测试结构相同。Protractor提供了全局的browser、element、by,分别用来打开页面和查找元素。

Protractor

describe('Protractor Demo App', function() {  it('should add one and two', function() {    browser.get('http://juliemr.github.io/protractor-demo/');    element(by.model('first')).sendKeys(1);    element(by.model('second')).sendKeys(2);    element(by.id('gobutton')).click();    expect(element(by.binding('latest')).getText()).        toEqual('5'); // This is wrong!  });});
  • by.model('first') 查找元素ng-model="first"
  • by.id('gobutton') 根据id查找元素
  • by.binding('latest') 查找绑定变量的元素 {{latest}}

2018上海马拉松

参考资料

Angular Testing
Jasmine Behavior-Driven JavaScript
Karma
Protractor - end-to-end testing for Angular

0