angularu在單元測試中如何模擬HTTP請求延遲

隨着對angular應用學習的深刻,如何在單元測試中模擬http請求延遲便提上了日程。在沒有http請求延遲之前,單元測試中咱們都是使用of()來手動發送數據的。of()方法在單元測試中無疑帶來了巨大的便利性,但因爲同步的機制,使其未能徹底的模擬中在生成環境中http請求延遲可能對組件帶來的衝擊,因此在啓用of()進行單元測試後沒法保障該組件在生產環境中是100%可靠運行的。html

閱讀本文須要對angular單元測試有必定了解。

sample

簡單舉個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層

對應的程序執行流程以下:
image.pngide

生產環境

而生產環境中因爲進行真正的http請求,該請求是異步的且必然有延遲,因此真實的控制檯狀況以下:函數

2ngOnInit執行完畢,開始渲染V層
1接收到了訂閱的數據

就便成了這樣:單元測試

image.png

也就是說:在有異步請求的狀況下,此單元測試並沒能保障該組件在生產環境中的正確運行。學習

接下來,本文將提供兩種模擬http請求延遲的方法。

沒有service的DEMO:[ https://stackblitz.com/edit/a...]( https://stackblitz.com/edit/a...

delay操做符

RxJS提供了delay操做符來延遲異步發送數據,因此模擬http請求延遲的最簡單的方法即是在of()方法的基礎上加入delay操做符。好比咱們將前面的測試樁修正爲:

getById(id: number): Observable<{name: string}> {
       const student = {name: 'hebut yunzhi'};
       
       // 延遲500MS異步發送數據
       return of(student).delay(500);
   }

此時便起到了延遲500MS異步發送數據的目的,因此在單元測試中執行相應的測試代碼執行流程以下:

image.png

如上圖,在單元測試中發生了異常。此異常提醒咱們組件在初始化的過程當中,沒有對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銷燬組件

執行流程以下:

image.png

怎樣才能保證在delay操做符500ms後發送數據時,組件並未銷燬並且能夠正常接收student並用接收到的學生渲染V層呢?

tick()模擬推動時鐘

在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的做用是模擬時鐘的推動,咱們測試其是否能夠影響RxJSdelay操做符

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 調度器補丁

RxJS應該是專門有一個本身的時間調度器(scheduler),該調度器做用於一系列與時間相關的操做符上。因此若是想在單元測試中模擬RxJS的時鐘推動,則須要在提供了個假的調度器來替換原有的真調度器。官方把這個操做稱爲patch--打補丁,具體的方案爲在對應的單元測試文件中import zone.js/dist/zone-patch-rxjs-fake-async。該文件的做用即是替換RxJS中原有的scheduler以達到能夠模擬進行時鐘推動的目的。

該方法可行,但打補丁並不正統,有興趣的可參考官方文檔嘗試。

彈珠測試

優秀偉大的RxJS爲咱們提供了RxJS marble testing(彈珠測試)以有效的在單元測試中手動控制數據的彈出。

使用marble testinggetById方法改寫爲:

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,該官方提供的方法很好的解決了上述問題。

本文做者:河北工業大學夢雲智開發團隊 潘傑
相關文章
相關標籤/搜索