Andular測試相關(guān)
常用斷言以及方法
- Jasmine 提供非常豐富的API,一些常用的Matchers:
toBe() 等同 ===
toNotBe() 等同 !==
toBeDefined() 等同 !== undefined
toBeUndefined() 等同 === undefined
toBeNull() 等同 === null
toBeTruthy() 等同 !!obj
toBeFalsy() 等同 !obj
toBeLessThan() 等同 <
toBeGreaterThan() 等同 >
toEqual() 相當(dāng)于 ==
toNotEqual() 相當(dāng)于 !=
toContain() 相當(dāng)于 indexOf
toBeCloseTo() 數(shù)值比較時定義精度,先四舍五入后再比較盔几。
toHaveBeenCalled() 檢查function是否被調(diào)用過
toHaveBeenCalledWith() 檢查傳入?yún)?shù)是否被作為參數(shù)調(diào)用過
toMatch() 等同 new RegExp().test()
toNotMatch() 等同 !new RegExp().test()
toThrow() 檢查function是否會拋出一個異常
而這些API之前用 not 來表示負(fù)值的判斷。
expect(true).not.toBe(false);
angular cli使用karma進(jìn)行單元測試.
-
使用
ng test
進(jìn)行測試袱瓮。端對端測試的命令是
ng e2e
常用參數(shù): --browsers 指定使用的瀏覽器 --code-coverage 輸出覆蓋率報告 --code-coverage-exclude 排除文件或路徑 --karma-config 指定Karma配置文件 --prod 啟用production環(huán)境 --progress 默認(rèn)為true,將編譯進(jìn)度輸出到控制臺 --watch 默認(rèn)為true爱咬,代碼修改后會重新運(yùn)行測試
默認(rèn)的測試文件擴(kuò)展名為.spec.ts尺借。
import { TestBed, async } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; import { StudyBarComponent } from './study-bar/study-bar.component'; describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ RouterTestingModule // 有路由的測試項(xiàng)需要用到 ], declarations: [ AppComponent, StudyBarComponent ], }).compileComponents(); })); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); }); it(`should have as title 'my-app'`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; expect(app.title).toEqual('my-app'); }); });
-
測試結(jié)構(gòu)
- describe函數(shù)中包含了beforeEach和it兩類函數(shù)。describe相當(dāng)于Java測試中的suite台颠,也就是測試組褐望,其中可以包含多個測試用例it。
- 一般一個測試文件含有一個describe串前,當(dāng)然也可以有多個。
- beforeEach相當(dāng)于Java測試中的@Before方法实蔽,每個測試用例執(zhí)行前調(diào)用一次荡碾。同樣,還有afterEach局装、beforeAll坛吁、afterAll函數(shù),afterEach在每個測試用例執(zhí)行后調(diào)用一次铐尚,beforeAll拨脉、afterAll相當(dāng)于Java測試中的@BeforeClass、@AfterClass方法宣增,每個describe執(zhí)行前后調(diào)用一次玫膀。
- **describe和it的第一個參數(shù)是測試說明。一個it中可以包含一個或多個expect來執(zhí)行測試驗(yàn)證爹脾。 **
-
TestBed
- TestBed.configureTestingModule()方法動態(tài)構(gòu)建TestingModule來模擬Angular @NgModule帖旨,支持@NgModule的大多數(shù)屬性箕昭。
- 測試中需導(dǎo)入測試的組件及依賴。例如在AppComponent頁面中使用了router-outlet解阅,因此我們導(dǎo)入了RouterTestingModule來模擬RouterModule落竹。Test Module預(yù)配置了一些元素,比如BrowserModule货抄,不需導(dǎo)入述召。
- TestBed.createComponent()方法創(chuàng)建組件實(shí)例,返回ComponentFixture蟹地。ComponentFixture是一個測試工具(test harness)积暖,用于與創(chuàng)建的組件和相應(yīng)元素進(jìn)行交互。
nativeElement和DebugElement
-
示例中使用了fixture.debugElement.nativeElement锈津,也可以寫成fixture.nativeElement呀酸。實(shí)際上,fixture.nativeElement是fixture.debugElement.nativeElement的一種簡化寫法琼梆。nativeElement依賴于運(yùn)行時環(huán)境性誉,Angular依賴DebugElement抽象來支持跨平臺。Angular創(chuàng)建DebugElement tree來包裝native element茎杂,nativeElement返回平臺相關(guān)的元素對象错览。我們的測試樣例僅運(yùn)行在瀏覽器中,因此nativeElement總為HTMLElement煌往,可以使用querySelector()倾哺、querySelectorAll()方法來查詢元素。
element.querySelector('p'); element.querySelector('input'); element.querySelector('.welcome'); element.querySelectorAll('span');
- detectChanges
-
createComponent() 函數(shù)不會綁定數(shù)據(jù)刽脖,必須調(diào)用fixture.detectChanges()來執(zhí)行數(shù)據(jù)綁定羞海,才能在組件元素中取得內(nèi)容:
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!'); });
當(dāng)數(shù)據(jù)模型值改變后,也需調(diào)用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 } ] });
啟用自動檢測后僅需在數(shù)值改變后調(diào)用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); });
-
依賴注入
-
對簡單對象進(jìn)行測試可以用new創(chuàng)建實(shí)例:
describe('ValueService', () => { let service: ValueService; beforeEach(() => { service = new ValueService(); }); ... });
不過大多數(shù)Service却邓、Component等有多個依賴項(xiàng),使用new很不方便院水。若用DI來創(chuàng)建測試對象腊徙,當(dāng)依賴其他服務(wù)時,DI會找到或創(chuàng)建依賴的服務(wù)檬某。要測試某個對象撬腾,在configureTestingModule中配置測試對象本身及依賴項(xiàng),然后調(diào)用TestBed.get()注入測試對象:
beforeEach(() => { TestBed.configureTestingModule({ providers: [ValueService] }); service = TestBed.get(ValueService); });
單元測試的原則之一:僅對要測試對象本身進(jìn)行測試恢恼,而不對其依賴項(xiàng)進(jìn)行測試民傻,依賴項(xiàng)通過mock方式注入,而不使用實(shí)際的對象,否則測試不可控饰潜。
Mock優(yōu)先使用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測試
- 僅測試組件類
測試組件類就像測試服務(wù)那樣簡單:
組件類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測試
只涉及類的測試可以判斷組件類的行為是否正常薯酝,但不能確定組件是否能正常渲染和交互半沽。
進(jìn)行組件DOM測試,需要使用TestBed.createComponent()等方法吴菠,第一個測試即為組件DOM測試者填。TestBed.configureTestingModule({ declarations: [ BannerComponent ] }); const fixture = TestBed.createComponent(BannerComponent); const component = fixture.componentInstance; expect(component).toBeDefined(); ``` **dispatchEvent** 為模擬用戶輸入,比如為input元素輸入值做葵,要找到input元素并設(shè)置它的 value 屬性占哟。Angular不知道你設(shè)置了input元素的value屬性,需要調(diào)用 dispatchEvent() 觸發(fā)輸入框的 input 事件酿矢,再調(diào)用 detectChanges(): ```typescript 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'); }); ``` - 嵌套組件 組件中常常使用其他組件: ```html <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>
對于無害的內(nèi)嵌組件可以直接將其添加到declarations中榨乎,這是最簡單的方式:
describe('AppComponent & TestModule', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent, BannerComponent, WelcomeComponent ] }) .compileComponents().then(() => { fixture = TestBed.createComponent(AppComponent); comp = fixture.componentInstance; }); })); ... });
也可為無關(guān)緊要的組件創(chuàng)建一些測試樁:
@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編譯器忽略那些不認(rèn)識的元素和屬性:
TestBed.configureTestingModule({ declarations: [ AppComponent, RouterLinkDirectiveStub ], schemas: [ NO_ERRORS_SCHEMA ] })
NO_ERRORS_SCHEMA方法比較簡單瘫筐,但不要過度使用蜜暑。NO_ERRORS_SCHEMA 會阻止編譯器因疏忽或拼寫錯誤而缺失的組件和屬性,如人工找出這些 bug會很費(fèi)時策肝。
RouterLinkDirectiveStubimport { 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肛捍,如只針對類測試不能證明指令的有效性。若通過組件來測試之众,單一的用例一般無法探索指令的全部能力拙毫。因此,更好的方法是創(chuàng)建一個能展示該指令所有用法的人造測試組件:
@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有關(guān)的測試還可以使用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(); }); ... });
-
-
在Mock的時候恬偷,優(yōu)先推薦使用Spy
使用方式簡介:
spyOn(obj, 'functionName').and.returnValue(returnValue);
spyOn(storeStub, 'select').and.callFake(func => { func(); return returnValue; });
在需要模擬返回值的地方使用spy監(jiān)視對象以及其調(diào)用的函數(shù),按上述兩種方式可以自定義返回值帘睦。
更詳細(xì)的用法參照