單元測試需要掌握的知識點
- karma.conf.js的配置 具體了解到每一項的意義,這樣才能真正的了解這個配置是如何配置的,甚至才可以做到自己的配置。
- 組件的測試
- 單獨的service測試
Angular的測試工具
Angular的測試工具類包含了TestBed類和一些輔助函數(shù)方法,當時這不是唯一的,你可以不依賴Angular 的DI(依賴注入)系統(tǒng),自己new出來測試類的實例。
孤立的單元測試
describe('Service: base-data-remote', () => {
let service = new BaseDataRemoteService();
it('should be created',() => {
expect(service).toBeTruthy();
});
});
利用Angular測試工具進行測試知識點總結
測試工具包含了TestBed類和@angular/core/testing中的一些方法。
- 在每個spec之前 ,
TestBed將自己重設為初始狀態(tài)。
測試組件
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創(chuàng)建BannerComponent組件的實例,可以用來測試和返回fixture。
- TestBed.createComponent關閉當前TestBed實例,讓它不能再被配置。
- query方法接受predicate函數(shù),并搜索fixture的整個DOM樹,試圖尋找第一個滿足predicate函數(shù)的元素。
- queryAll方法返回一列數(shù)組,包含所有DebugElement中滿足predicate的元素。
- By類是Angular測試工具之一,它生成有用的predicate。 它的By.css靜態(tài)方法產(chǎn)生標準CSS選擇器 predicate,與JQuery選擇器相同的方式過濾。
-
detectChanges:在測試中的Angular變化檢測。
每個測試程序都通過調(diào)用fixture.detectChanges()
來通知Angular執(zhí)行變化檢測。
測試有依賴的組件,這個依賴的測試
這個依賴的模擬方式有兩種:偽造服務實例(提供服務復制品)、刺探真實服務。這兩種方式都不錯,只需要挑選一種最適合你當前測試文件的測試方式來做最好。
偽造服務實例
被測試的組件不一定要注入真正的服務。實際上,服務的復制品(stubs, fakes, spies或者mocks)通常會更加合適。 spec的主要目的是測試組件,而不是服務。真實的服務可能自身有問題。
這個測試套件提供了最小化的UserServiceStub類,用來滿足組件和它的測試的需求。
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
獲取注入的服務
測試程序需要訪問被注入到組件中的UserService(stub類)。
Angular的注入系統(tǒng)是層次化的。 可以有很多層注入器,從根TestBed創(chuàng)建的注入器下來貫穿整個組件樹。
最安全并總是有效的獲取注入服務的方法,是從被測試的組件的注入器獲取。 組件注入器是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的設計是,所有調(diào)用getTodos的方法都會受到立刻解析的承諾,得到一條預設的名言。
it方法中的幾個函數(shù)
寫單元測試時,it里經(jīng)常會有幾個常見的方法,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的一部分。通過將測試代碼放到特殊的異步測試區(qū)域來運行,async函數(shù)簡化了異步測試程序的代碼。
- 接受無參數(shù)的函數(shù)方法,返回無參數(shù)的函數(shù)方法,變成Jasmine的it函數(shù)的參數(shù)。
- 它的參數(shù)看起來和普通的it參數(shù)主體一樣。 沒有任何地方顯示異步特征。 比如,它不返回承諾,并且沒有done方法可調(diào)用,因為它是標準的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”的意思是當所有待處理異步行為完成時的狀態(tài),在“stable”后whenStable承諾被解析。 - 然后測試程序繼續(xù)運行,并開始另一輪的變化檢測(fixture.detectChanges
),通知Angular使用名言來更新DOM。 getQuote
輔助方法提取出顯示元素文本,然后expect語句確認這個文本與預備的名言相符。 - fakeAsync
- fakeAsync是另一種Angular測試工具。
- 和async一樣,它也接受無參數(shù)函數(shù)并返回一個函數(shù),變成Jasmine的it
函數(shù)的參數(shù)。 - fakeAsync函數(shù)通過在特殊的fakeAsync測試區(qū)域運行測試程序,讓測試代碼更加簡單直觀。
- 對于async來說,fakeAsync最重要的好處是測試程序看起來像同步的。里面沒有任何承諾。 沒有then(...)鏈來打斷控制流。
- tick
tick函數(shù)是Angular測試工具之一,是fakeAsync的同伴。 它只能在fakeAsync的主體中被調(diào)用。 - 調(diào)用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函數(shù)大大簡化了異步測試,但是你仍然可以使用傳統(tǒng)的Jasmine異步測試技術。
你仍然可以將接受 done回調(diào)的函數(shù)傳給it。 但是,你必須鏈接承諾、處理錯誤,并在適當?shù)臅r候調(diào)用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函數(shù)
注意beforeEach里面對async的調(diào)用,因為異步方法TestBed.compileComponents而變得必要。
compileComponents
- 在本例中,TestBed.compileComponents編譯了組件,那就是DashbaordComponent。 它是這個測試模塊唯一的聲明組件。
- 本章后面的測試程序有更多聲明組件,它們中間的一些導入應用模塊,這些模塊有更多的聲明組件。 一部分或者全部組件可能有外部模板和CSS文件。 TestBed.compileComponents一次性異步編譯所有組件。
- compileComponents方法返回承諾,可以用來在它完成時候,執(zhí)行更多額外任務。
測試帶有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,它代表了英雄所在的<div>。 測試程序用"click"事件名字來調(diào)用triggerEventHandler。 調(diào)用DashboardHeroComponent.click()時,"click"事件綁定作出響應。
如果組件想期待的那樣工作,click()通知組件的selected屬性發(fā)出hero對象,測試程序通過訂閱selected事件而檢測到這個值,所以測試應該成功。
triggerEventHandler
Angular的DebugElement.triggerEventHandler可以用事件的名字觸發(fā)任何數(shù)據(jù)綁定事件。 第二個參數(shù)是傳遞給事件處理器的事件對象。
自己遇到的坑兒
下面都是自己在實際的編寫單元測試時,真實遇到的問題,自己真的是在這上面花費了很多時間?。。?!為什么沒有說花冤枉時間呢?就是因為是自己對單元測試還沒喲掌握,所以出了錯,不要緊,重要的是以后不能再犯!
service的注入
剛剛接觸angular2吧,對很多service的寫法不是很了解,以至于真的是白白浪費了很多時間,尤其是在這個service的模擬上??赡苈斆魅缒?,不會犯我這樣簡單卻又致命的錯誤吧,只希望,以后的賀賀也可以不再犯這樣的錯!??自己一把... ...
首先來看一下,我創(chuàng)建的這個service的用法。
@Injectable()
export class BaseDataService {
remoteService: BaseDataRemoteService;
private datasMap = {}; // 用于存儲所有的數(shù)據(jù)
private todosCache = {}; // 待辦數(shù)據(jù)的id的臨時存儲
private draftsCache = {}; // 草稿數(shù)據(jù)臨時存儲
private relatedCache = {}; // 已辦理數(shù)據(jù)臨時存儲
constructor(private config: any, private http: Http) {
this.config = config;
this.config.baseUrl = config.baseUrl || config.name;
this.remoteService = new BaseDataRemoteService(this.config.baseUrl, this.config.idPropertyName, this.http);
}
getTodos(userId: String, pageNo?: number): any {
pageNo = pageNo || 0;
return this.remoteService.getTodos(userId, pageNo).then(datas => {
let todos = datas.content;
todos.forEach(element => {
this.datasMap[element[this.config.idPropertyName]] = element;
if (pageNo === 0) {
this.todosCache = {};
}
});
this.todosCache[pageNo] = this.getRecordIds(todos);
return todos;
}, () => {
return [];
});
}
}
其中的
BaseDataRemoteService我是自己new出來的,而且這個BaseDataService也是我自己new出來的,所以首先第一點,我應該自己創(chuàng)建,而不能使用angular的DI系統(tǒng)來幫助我創(chuàng)建。
第二點就是在模擬的時候,我竟然傻傻的自己去在spec文件中自己去new了
BaseDataRemoteService,所以我根本沒有辦法去執(zhí)行spyOn(foo, "getBar")這樣的模擬,然后就是一直的出錯。錯樣百出了!
正確的單元測試:
function makeEnvironment() {
return TestBed.configureTestingModule({
providers: [
MockBackend,
BaseRequestOptions,
{
provide: Http,
useFactory: (backend, options) => {
return new Http(backend, options);
}, deps: [MockBackend, BaseRequestOptions],
},
],
imports: [HttpModule],
});
}
const userId: String = '123';
let pageNo: number;
describe('Service: base-data', () => {
const config = {
name: 'archives/out',
baseUrl: '/archives/out',
idPropertyName: 'outId',
subflagPropertyName: 'subflag',
};
const datas = [];
const datas2 = [];
let service: BaseDataService;
let spy: jasmine.Spy;
let http: Http; // 還應該是DI系統(tǒng)的
beforeEach(() => {
const testBed = makeEnvironment();
http = testBed.get(Http);
service = new BaseDataService(config, http); //這是自己new出來的
// 但是自己不能new出來BaseDataRemoteService
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('獲取到的數(shù)據(jù)為空', async(() => {
// 這樣的使用才是正確的!?。? spy = spyOn(service.remoteService, 'getTodos').and.returnValue(Promise.resolve({content: []}));
service.getTodos(userId).then(todos => {
expect(todos.length).toBe(0);
expect(todos).toEqual([]);
expect(service.getTodosCache(0).length).toBe(0);
});
}));
下次一定要注意,不要瞎寫?。?!
多次調(diào)用同一個異步方法
相信大家對這段單元測試的代碼很熟悉,這里就是模擬多次調(diào)用同一個方法時,返回不同的值。
這里是同步方法的模擬返回數(shù)據(jù),那么異步方法同樣可以。
describe("A spy, when configured to fake a series of return values", function() {
var foo, bar;
beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
},
getBar: function() {
return bar;
}
};
// 多次調(diào)用時,返回不同的值!
spyOn(foo, "getBar").and.returnValues("fetched first", "fetched second");
foo.setBar(123);
});
it("when called multiple times returns the requested values in order", function() {
expect(foo.getBar()).toEqual("fetched first");
expect(foo.getBar()).toEqual("fetched second");
expect(foo.getBar()).toBeUndefined();
});
});
下面是出錯的代碼。
it('when the baseUrl is exist and pageNo is exist', async(() => {
// 模擬多次進行異步調(diào)用時的返回值
spyOn(service.remoteService, 'getRelatedList').and
.returnValues(Promise.resolve({content: datas}), Promise.resolve({content: datas2}));
pageNo = 0;
// 第一次調(diào)用
service.getRelatedList(userId, pageNo).then(relate => {
expect(relate.length).toBe(2);
expect(relate).toEqual(datas);
expect(service.getRelatedCache(0).length).toBe(2, 'the length should be 2.');
});
// 第二次調(diào)用
service.getRelatedList(userId).then(relate => {
expect(relate.length).toBe(3);
expect(relate).toEqual(datas2);
expect(relate.length).not.toBe(2);
expect(relate).not.toEqual(datas);
expect(service.getRelatedCache(0).length).toBe(3);
});
}));
下面是單元測試的結果:

雖然第一個、第二個expect通過了,但是第三個無論如何也通不過。其實不是代碼寫的有問題,是單元測試寫的有有問題,在第一個expect去判斷的時候,第二個
service.getRelatedList已經(jīng)執(zhí)行完了,所以才會出錯。
這個錯誤,我意識到了,所以我再第二次調(diào)用的地方添加了一個延時執(zhí)行的函數(shù),這樣單元測試是完全正確的,但是這并不是一個好的解決辦法。
setTimeout(function() {
}, 200);
最好的解決辦法是,是使用
fakeAsync和tick來解決。
tick函數(shù)是Angular測試工具之一,是fakeAsync的同伴。 它只能在fakeAsync的主體中被調(diào)用。
調(diào)用tick()模擬時間的推移,直到全部待處理的異步任務都已完成。
下面是正確的代碼:
it('when the baseUrl is exist and pageNo is exist', fakeAsync(() => {
spyOn(service.remoteService, 'getRelatedList').and
.returnValues(Promise.resolve({content: datas}), Promise.resolve({content: datas2}));
pageNo = 0;
service.getRelatedList(userId, pageNo).then(relate => {
expect(relate.length).toBe(2);
expect(relate).toEqual(datas);
expect(service.getRelatedCache(0).length).toBe(2, 'the length should be 2.');
});
tick(); // 基本的意思就是,前后分開來執(zhí)行
service.getRelatedList(userId).then(relate => {
expect(relate.length).toBe(3);
expect(relate).toEqual(datas2);
expect(relate.length).not.toBe(2);
expect(relate).not.toEqual(datas);
expect(service.getRelatedCache(0).length).toBe(3);
});
}));
其實這之前我是把
tick()方法都看過一遍的,可是還是不理解其中的意思,所以記錄下來吧,所謂書讀百遍變其義自見,加油啊!??????
未完待續(xù)...