Angular單元測試與E2E測試
本文介紹了Angular單元測試和E2E測試的配置與測試方法。示例APP使用Angular 7 CLI建立,已配置好基礎測試環境,生成了測試樣例程式碼。預設,Angular單元測試使用Jasmine測試框架和Karma測試執行器,E2E測試使用Jasmine測試框架和Protractor端到端測試框架。
配置單元測試
Jasmine是一個用於測試JavaScript的行為驅動開發框架,不依賴於任何其他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'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), require('@angular-devkit/build-angular/plugins/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 install 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中檢視測試情況。
編寫測試
第一個測試
使用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 h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').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 h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to hello!');
});
當資料模型值改變後,也需呼叫fixture.detectChanges()方法:
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
app.title = 'china';
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').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(h1.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(h1.textContent).toContain(oldTitle);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
fixture.detectChanges(); // detect changes explicitly
expect(h1.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<AppComponent>;
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 h1 tag', () => {
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').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
同測試含其它依賴的物件一樣可以使用spy方式:
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');
});
巢狀元件
元件中常常使用其他元件:
<app-banner></app-banner>
<app-welcome></app-welcome>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
<a routerLink="/about">About</a>
</nav>
<router-outlet></router-outlet>
對於無害的內嵌元件可以直接將其新增到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會很費時。
屬性指令測試
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: `
<h2 highlight="yellow">Something Yellow</h2>
<h2 highlight>The Default (Gray)</h2>
<h2>No Highlight</h2>
<input #box [highlight]="box.value" value="cyan"/>`
})
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 h2 without the HighlightDirective
bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
});
// color tests
it('should have three highlighted elements', () => {
expect(des.length).toBe(3);
});
it('should color 1st <h2> background "yellow"', () => {
const bgColor = des[0].nativeElement.style.backgroundColor;
expect(bgColor).toBe('yellow');
});
it('should color 2nd <h2> 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 <input> 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 <h2> 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<Data>(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 install -g protractor
在專案中執行npm install時會安裝protractor,不必單獨執行以上命令。安裝protractor後會安裝兩個命令列工具protractor和webdriver-manager(位於node_modules\protractor\bin目錄),webdriver-manager負責管理驅動、啟停Selenium Server。
webdriver-manager命令:
clean removes all downloaded driver files from the out_dir
start start up the selenium server
shutdown shut down the selenium server
status list the current available drivers
update install or update selected binaries,更新的驅動儲存在node_modules\protractor\node_modules\webdriver-manager\selenium目錄下
version get the current version
配置檔案
使用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 update
webdriver-manager start
刪除原配置中的directConnect、baseUrl:
directConnect: true,
baseUrl: 'http://localhost:4200/',
增加seleniumAddress(預設為http://localhost:4444/wd/hub):
seleniumAddress: 'http://localhost:4444/wd/hub',
執行測試
執行CLI命令 ng e2e即可執行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.
指定配置檔案
不同的環境若配置不同,可使用不同的配置檔案。
比如,在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
編寫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 h1')).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}}
參考資料
Angular Testing
Jasmine Behavior-Driven JavaScript
Karma
Protractor - end-to-end testing for Angular