隨着對angular應用學習的深刻,如何在單元測試中模擬http請求延遲便提上了日程。在沒有http請求延遲之前,單元測試中咱們都是使用of()
來手動發送數據的。of()
方法在單元測試中無疑帶來了巨大的便利性,但因爲同步
的機制,使其未能徹底的模擬中在生成環境中http請求延遲可能對組件帶來的衝擊,因此在啓用of()
進行單元測試後沒法保障該組件在生產環境中是100%可靠運行的。html
閱讀本文須要對angular單元測試有必定了解。
簡單舉個of()
的例子來查看其如何成爲異步請求單元測試中的不可靠元素。typescript
V層顯示學生的姓名npm
<h1>{{student.name}}</h1>
C層初始化學生的值爲undefinedjson
/** * 該值初始化爲undefined */ student: {name: string}; ngOnInit() { // 調用服務來獲取ID爲1的學生 this.studentService.getById(1) .subscribe(student => { console.log('1接收到了訂閱的數據'); this.student = student; }) console.log('2ngOnInit執行完畢,開始渲染V層');
用於單元測試的測試樁StudentStubService異步
getById(id: number): Observable<{name: string}> { const student = {name: 'hebut yunzhi'}; // 使用of()方法來返回可觀察者,該觀察者在被訂閱時將同步發送數據 return of(student); }
控制檯打印結果以下:async
1接收到了訂閱的數據 2ngOnInit執行完畢,開始渲染V層
對應的程序執行流程以下:
ide
而生產環境中因爲進行真正的http請求,該請求是異步的且必然有延遲,因此真實的控制檯狀況以下:函數
2ngOnInit執行完畢,開始渲染V層 1接收到了訂閱的數據
就便成了這樣:單元測試
也就是說:在有異步請求的狀況下,此單元測試並沒能保障該組件在生產環境中的正確運行。學習
接下來,本文將提供兩種模擬http請求延遲的方法。
沒有service的DEMO:[ https://stackblitz.com/edit/a...]( https://stackblitz.com/edit/a...
RxJS
提供了delay
操做符來延遲異步發送數據,因此模擬http請求延遲的最簡單的方法即是在of()
方法的基礎上加入delay
操做符。好比咱們將前面的測試樁修正爲:
getById(id: number): Observable<{name: string}> { const student = {name: 'hebut yunzhi'}; // 延遲500MS異步發送數據 return of(student).delay(500); }
此時便起到了延遲500MS異步發送數據的目的,因此在單元測試中執行相應的測試代碼執行流程以下:
如上圖,在單元測試中發生了異常。此異常提醒咱們組件在初始化的過程當中,沒有對student進行正確的初始化。爲了防止生產環境中發生異常的錯誤,特對組件修正以下:
/** * 初始化學生,以防止在組件初始化過程當中V層渲染髮生undefined異常 */ student = {} as {name: string}; ngOnInit() { this.studentService.getById(1) .subscribe(student => { // 生產環境中,如下代碼將在必定延遲後被異步執行被執行。 this.student = student; })
以上代碼保證了組件初始化的過程未發生異常。
但因爲delay
的異步執行機制,當delay
方法在500ms返回數據時,單元測試的方法已經執行完畢了且組件已經由內存釋放了。也就是說:雖然返回了數據,但因爲接收數據的組件已經不存在了,因此該數據並不能體如今被測試組件的視圖中。
簡單來說就是:咱們沒法在單元測試中來查看、斷言studentService.getById
的返回值是符合預期並期可以支持組件正常工做的。
ngOnInit() { // 調用服務來獲取ID爲1的學生 this.studentService.getById(1) .subscribe(student => { console.log('1接收到了訂閱的數據'); this.student = student; }) console.log('2ngOnInit執行完畢,開始渲染V層'); it('should create', () => { console.log('3斷言組件初始化成功'); expect(component).toBeTruthy(); }); afterEach(() => { console.log('4銷燬組件'); fixture.destroy(); });
執行結果:
2ngOnInit執行完畢,開始渲染V層 3斷言組件初始化成功 4銷燬組件
執行流程以下:
怎樣才能保證在delay操做符500ms後發送數據時,組件並未銷燬並且能夠正常接收student並用接收到的學生渲染V層呢?
在angular單元測試中爲咱們提供了tick()
方法來模擬時鐘的推動。該方法須要配合fakeAsync
使用,好比:
it('tick test', fakeAsync(() => { let a = 1; // 500ms後,將a的值變爲2 setTimeout(() => { a = 2; // ➊ }, 500); // 斷言a的值未發生變化,值爲1 expect(a).toEqual(1); // 使用tick模擬將時鐘推動500ms,➊的代碼被執行。 tick(500); // 斷言a的值發生變化,值爲2 expect(a).toEqual(2); }));
既然tick
的做用是模擬時鐘的推動,咱們測試其是否能夠影響RxJS
的delay
操做符
it('should create', fakeAsync(() => { expect(component).toBeTruthy(); // 斷言因爲delay操做符的緣由,commpont.student的值仍然初始化的值:null expect(component.student).toBeNull(); // 模擬將時鐘推動500ms tick(500); // 若是tick對rxjs的delay操做符起做用,那麼如下斷言經過。 // 若是不起做用,那麼如下斷言執行失敗。 expect(component.student).toBeTruthy(); }));
最終的實驗結果是以上代碼沒法經過單元測試,即:tick函數並不對delay操做符起做用。
這本質上是因爲RxJS在進行一些延遲
處理的時候,並無使用js內置的setTimeout等方法,而tick方法進行的模擬時鐘推動又僅能在setTimeout等方法上生效,因此:tick方法並不可以影響RxJS的在時間
上的處理進程。
RxJS應該是專門有一個本身的時間調度器(scheduler),該調度器做用於一系列與時間相關的操做符上。因此若是想在單元測試中模擬RxJS的時鐘推動,則須要在提供了個假的調度器
來替換原有的真調度器
。官方把這個操做稱爲patch
--打補丁,具體的方案爲在對應的單元測試文件中import zone.js/dist/zone-patch-rxjs-fake-async
。該文件的做用即是替換RxJS中原有的scheduler以達到能夠模擬進行時鐘推動的目的。
該方法可行,但打補丁
並不正統,有興趣的可參考官方文檔嘗試。
優秀偉大的RxJS爲咱們提供了RxJS marble testing(彈珠測試)
以有效的在單元測試中手動控制數據的彈出。
使用marble testing
將getById
方法改寫爲:
marbles可能並未包含在angular的默認package.json中,若是是這樣的話,須要手動install:
npm install jasmine-marbles
getById(id: number): Observable<{name: string}> { const student = {name: 'hebut yunzhi'}; // 彈珠測試:等待3個時鐘週期(-)後發送數據x,x的值爲student。而後發送完成發送(|) return code('---x|', {x: student}); }
對應單元測試方法修改成:
it('should create', fakeAsync(() => { expect(component).toBeTruthy(); // 斷言因爲delay操做符的緣由,commpont.student的值仍然初始化的值:null expect(component.student).toBeNull(); // RxJS彈珠測試發送數據 getTestScheduler().flush(); // 斷言student發生了變動 expect(component.student).toBeTruthy(); expect(component.student.name).toEqual(''hebut yunzhi); }));
如上所示,在單元測試中調用了getTestScheduler().flush();
來完成了彈珠測試。如此以來,上述代碼高度模擬了http異步請求,高度的與生產環境相一致。單元測試有效的保障了生產環境整個項目的健壯性。
it('should create', () => // 以下斷言保障了組件在初始化的過程當中未發生異常 expect(component).toBeTruthy(); // 模擬生產環境後臺異步返回數據 getTestScheduler().flush(); // 保障接收模擬數據後,組件從新渲染未發生異常 fixture.detectChanges(); });
在實際的生產項目中,有一組件在單元測試徹底OK的狀況下卻在線上報了undefined錯誤。追蹤其緣由時發現是由of
方法的同步返回數據引發了。爲了更好的貼近於生產項目,在單元測試中如何引用異步測試便擺在了眼前。
因爲RxJS對時間處理採用了調度器的機制,因此原對setTimeout等方法起做用的tick方法並不能推動RxJS的計時器,從而使得在單元測試中使用RxJS異步測試組件的健壯性。
使用RxJS進行單元測試的正確方法爲使用marble testing
,該官方提供的方法很好的解決了上述問題。
本文做者:河北工業大學夢雲智開發團隊 潘傑