本文介紹了Angular 7單元測試和E2E測試的配置與測試方法。使用Angular CLI新建工程,已配置好基礎測試環境,生成了測試樣例代碼。默認,Angular單元測試使用Jasmine測試框架和Karma測試運行器,E2E測試使用Jasmine和Protractor測試框架。css
Jasmine是用於測試JavaScript的行爲驅動(Behavior-Driven)框架,不依賴於任何其餘JavaScript框架。
Karma是測試運行器,爲開發人員提供了高效、真實的測試環境,支持多種瀏覽器,易於調試。html
單元測試配置文件test.ts和karma.conf.js:
test.tsnode
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.jsandroid
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環境。git
Karma支持的瀏覽器:github
可同時配置多個瀏覽器進行測試,要啓用其餘瀏覽器,需安裝依賴,好比啓用Firefox:web
npm i karma-firefox-launcher --save-dev
而後在karma.conf.js內增長配置:chrome
... require('karma-chrome-launcher'), require('karma-firefox-launcher'), ... browsers: ['Chrome', 'Firefox'], ...
用CLI建立App生成了一個單元測試文件app.component.spec.ts。執行CLI命令ng test便可運行單元測試:npm
ng test
運行後在控制檯輸出測試結果並打開瀏覽器:
瀏覽器會顯示測試結果,總測試數,失敗數。在頂部,每一個點或叉對應一個測試用例,點表示成功,叉表示失敗,鼠標移到點或叉上會顯示測試信息。點擊測試結果中的某一行,可從新運行某個或某組(測試套件)測試。json
經常使用參數:
--browsers 指定使用的瀏覽器
--code-coverage 輸出覆蓋率報告
--code-coverage-exclude 排除文件或路徑
--karma-config 指定Karma配置文件
--prod 啓用production環境
--progress 默認爲true,將編譯進度輸出到控制檯
--watch 默認爲true,代碼修改後會從新運行測試
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 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(); }); }));
對簡單對象進行測試能夠用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); });
同測試含其它依賴的對象同樣,能夠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(); }));
測試組件類就像測試服務那樣簡單:
組件類
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測試,須要使用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會很費時。
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: ` <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(); });
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'); }); ... });
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測試使用Jasmine和Protractor測試框架,Protractor是Angular端到端測試框架。
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_dir start start up the selenium server shutdown shut down the selenium server status list the current available drivers update update selected binaries version 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。
使用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',
運行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 -g npm 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
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,分別用來打開頁面和查找元素。
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! }); });
2018上海馬拉松
Angular Testing
Jasmine Behavior-Driven JavaScript
Karma
Protractor - end-to-end testing for Angular