單元測試需要掌握的知識點
- karma.conf.js的配置 具體瞭解到每一項的意義,這樣才能真正的瞭解這個配置是如何配置的,甚至纔可以做到自己的配置。
- 組件的測試
- 單獨的service測試
Angular的測試工具
Angular的測試工具類包含了TestBed
類和一些輔助函數方法,當時這不是唯一的,你可以不依賴Angular 的DI(依賴注入)系統,自己new出來測試類的實例。
孤立的單元測試
describe('Service: base-data-remote', () => {
let service = new BaseDataRemoteService();
it('should be created',() => {
expect(service).toBeTruthy();
});
});
利用Angular測試工具進行測試知識點總結
測試工具包含了TestBed
類和@angular/core/testing
中的一些方法。
- 在每個spec之前 ,
TestBed
將自己重設爲初始狀態。
測試組件
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
describe('BannerComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
});
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
});
- 組件測試
- TestBed.createComponent創建BannerComponent組件的實例,可以用來測試和返回fixture。
- TestBed.createComponent關閉當前TestBed實例,讓它不能再被配置。
- query方法接受predicate函數,並搜索fixture的整個DOM樹,試圖尋找第一個滿足predicate函數的元素。
- queryAll方法返回一列數組,包含所有DebugElement中滿足predicate的元素。
- By類是Angular測試工具之一,它生成有用的predicate。 它的By.css靜態方法產生標準CSS選擇器 predicate,與JQuery選擇器相同的方式過濾。
- detectChanges:在測試中的Angular變化檢測。
每個測試程序都通過調用fixture.detectChanges()
來通知Angular執行變化檢測。
測試有依賴的組件,這個依賴的測試
這個依賴的模擬方式有兩種:僞造服務實例(提供服務複製品)、刺探真實服務。這兩種方式都不錯,只需要挑選一種最適合你當前測試文件的測試方式來做最好。
僞造服務實例
被測試的組件不一定要注入真正的服務。實際上,服務的複製品(stubs, fakes, spies或者mocks)通常會更加合適。 spec的主要目的是測試組件,而不是服務。真實的服務可能自身有問題。
這個測試套件提供了最小化的UserServiceStub類,用來滿足組件和它的測試的需求。
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
獲取注入的服務
測試程序需要訪問被注入到組件中的UserService(stub類)。
Angular的注入系統是層次化的。 可以有很多層注入器,從根TestBed創建的注入器下來貫穿整個組件樹。
最安全並總是有效的獲取注入服務的方法,是從被測試的組件的注入器獲取。 組件注入器是fixture的DebugElement的屬性。
出人意料的是,請不要引用測試代碼裏提供給測試模塊的userServiceStub對象。它是行不通的! 被注入組件的userService實例是徹底不一樣的對象,是提供的userServiceStub
的克隆。
- TestBed.get方法從根注入器中獲取服務。
例如:
dataService = testBed.get(DataService);
測試代碼
beforeEach(() => {
// stub UserService for test purposes
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
// 重點
providers: [ {provide: UserService, useValue: userServiceStub } ]
});
fixture = TestBed.createComponent(WelcomeComponent);
comp = fixture.componentInstance;
// UserService from the root injector
// 重點
userService = TestBed.get(UserService);
// get the "welcome" element by CSS selector (e.g., by class name)
de = fixture.debugElement.query(By.css('.welcome'));
el = de.nativeElement;
});
刺探(Spy)真實服務
注入了真是的服務,並使用Jasmine的spy
替換關鍵的getXxxx
方法。
spy = spyOn(remoteService, 'getTodos').and.returnValues([Promise.resolve(datas), Promise.resolve(datas2)]);
Spy
的設計是,所有調用getTodos
的方法都會受到立刻解析的承諾,得到一條預設的名言。
it方法中的幾個函數
寫單元測試時,it裏經常會有幾個常見的方法,async()
,fakeAsync()
,tick()
,jasmine.done()
方法等。
這幾個方法,都幫助我們簡化了異步測試程序的代碼。但是需要正確的使用這幾個方法。
組件
@Component({
selector: 'twain-quote',
template: '<p class="twain"><i>{{quote}}</i></p>'
})
export class TwainComponent implements OnInit {
intervalId: number;
quote = '...';
constructor(private twainService: TwainService) { }
ngOnInit(): void {
this.twainService.getQuote().then(quote => this.quote = quote);
}
}
- async
- 是Angular TestBed的一部分。通過將測試代碼放到特殊的異步測試區域來運行,async函數簡化了異步測試程序的代碼。
- 接受無參數的函數方法,返回無參數的函數方法,變成Jasmine的it函數的參數。
- 它的參數看起來和普通的it參數主體一樣。 沒有任何地方顯示異步特徵。 比如,它不返回承諾,並且沒有done方法可調用,因爲它是標準的Jasmine異步測試程序。
使用例子:
it('should show quote after getQuote promise (async)', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
});
}));
- 簡單介紹一下whenStable()方法
- 測試程序必須等待getQuote在JavaScript引擎的下一回閤中被解析。
- ComponentFixture.whenStable方法返回它自己的承諾,它getQuote
承諾完成時被解析。實際上,“stable”的意思是當所有待處理異步行爲完成時的狀態,在“stable”後whenStable承諾被解析。 - 然後測試程序繼續運行,並開始另一輪的變化檢測(fixture.detectChanges
),通知Angular使用名言來更新DOM。 getQuote
輔助方法提取出顯示元素文本,然後expect語句確認這個文本與預備的名言相符。
- fakeAsync
- fakeAsync是另一種Angular測試工具。
- 和async一樣,它也接受無參數函數並返回一個函數,變成Jasmine的it
函數的參數。 - fakeAsync函數通過在特殊的fakeAsync測試區域運行測試程序,讓測試代碼更加簡單直觀。
- 對於async來說,fakeAsync最重要的好處是測試程序看起來像同步的。裏面沒有任何承諾。 沒有then(…)鏈來打斷控制流。
- tick
tick函數是Angular測試工具之一,是fakeAsync的同伴。 它只能在fakeAsync的主體中被調用。
- 調用tick()模擬時間的推移,直到全部待處理的異步任務都已完成,在這個測試案例中,包含getQuote承諾的解析。
使用例子
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
fixture.detectChanges();
tick(); // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
}));
- jasmine.done
雖然async和fakeAsync函數大大簡化了異步測試,但是你仍然可以使用傳統的Jasmine異步測試技術。
你仍然可以將接受 done回調的函數傳給it。 但是,你必須鏈接承諾、處理錯誤,並在適當的時候調用done。
使用例子
it('should show quote after getQuote promise (done)', done => {
fixture.detectChanges();
// get the spy promise and wait for it to resolve
spy.calls.mostRecent().returnValue.then(() => {
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
done();
});
});
以上這三個測試例子是等價的,也就是說,你可以隨你喜好選擇你喜歡的測試方式來進行單元測試的編寫。
測試有外部模板的組件
使用例子
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent ],
})
.compileComponents(); // compile template and css
}));
beforeEach裏的async函數
注意beforeEach裏面對async的調用,因爲異步方法TestBed.compileComponents而變得必要。
compileComponents
- 在本例中,TestBed.compileComponents編譯了組件,那就是DashbaordComponent。 它是這個測試模塊唯一的聲明組件。
- 本章後面的測試程序有更多聲明組件,它們中間的一些導入應用模塊,這些模塊有更多的聲明組件。 一部分或者全部組件可能有外部模板和CSS文件。 TestBed.compileComponents一次性異步編譯所有組件。
- compileComponents方法返回承諾,可以用來在它完成時候,執行更多額外任務。
測試帶有inputs和outputs的組件
測試前期代碼
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent ],
})
.compileComponents(); // compile template and css
}));
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(DashboardHeroComponent);
comp = fixture.componentInstance;
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero element
// pretend that it was wired to something that supplied a hero
expectedHero = new Hero(42, 'Test Name');
comp.hero = expectedHero;
fixture.detectChanges(); // trigger initial data binding
});
屬性
測試代碼是將模擬英雄(expectedHero)賦值給組件的hero
屬性的。
// pretend that it was wired to something that supplied a hero
expectedHero = new Hero(42, 'Test Name');
comp.hero = expectedHero;
點擊事件
it('should raise selected event when clicked', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
heroEl.triggerEventHandler('click', null);
expect(selectedHero).toBe(expectedHero);
});
這個組件公開EventEmitter屬性。測試程序像宿主組件那樣來描述它。
heroEl是個DebugElement,它代表了英雄所在的
如果組件想期待的那樣工作,click()通知組件的selected屬性發出hero對象,測試程序通過訂閱selected事件而檢測到這個值,所以測試應該成功。
triggerEventHandler
Angular的DebugElement.triggerEventHandler可以用事件的名字觸發任何數據綁定事件。 第二個參數是傳遞給事件處理器的事件對象。
自己遇到的坑兒
下面都是自己在實際的編寫單元測試時,真實遇到的問題,自己真的是在這上面花費了很多時間啊!!!爲什麼沒有說花冤枉時間呢?就是因爲是自己對單元測試還沒喲掌握,所以出了錯,不要緊,重要的是以後不能再犯!
service的注入
剛剛接觸angular2吧,對很多service的寫法不是很瞭解,以至於真的是白白浪費了很多時間,尤其是在這個service的模擬上。可能聰明如你,不會犯我這樣簡單卻又致命的錯誤吧,只希望,以後的賀賀也可以不再犯這樣的錯!
寫在後面
GitHub上集大家之力搞了一個前端面試題的項目,裏面都是大家面試時所遇到的題以及一些學習資料,有興趣的話可以關注一下。如果你也有興趣加入我們的話,請在項目中留言。項目同時也可以在gitbook上查看。