Angular Testing 筆記 --- 7 關於 Component with async 的測試。

來由

在前端的服務裡,我們很常會透過 API 跟後端拿取資料,不管是登入的認證或是一些資料的顯示都必須透過網路這種會有延遲、非同步的、必須等待的方式與服務。所以在非同步的 service 我們要怎麼做測試呢?這次就來撰寫有關 async 的測試吧!

加入非同步 service

我們在 demo.service.ts 內加入一個非同步的認證服務來當作 async 的 service 如下:

demo.service.ts
// rxjs style asynAuthenticated(): Observable<boolean> { return of(!!localStorage.getItem('token')) .pipe(delay(3000)); }

上述的程式碼會在 3 秒後才會回傳 localStorage 是否有沒有 token 這個物件。有的話我們就認為認證成功,沒有的話就否。

所以接下來我們修改 login.component.ts 內的 onSubmit() 原先認證的方式,由 isAuthenticated() 改為 asynAuthenticated()

login.components.ts   
onSubmit() { localStorage.setItem('token', JSON.stringify({ email: this.loginForm.value.email}));
// // sync authenticated // if (this.demo.isAuthenticated()) { // this.isAuthenticated = true; // } else { // this.isAuthenticated = false; // } // rxjs style this.demo.asynAuthenticated().subscribe( res => { if (res) { this.isAuthenticated = true; } else { this.isAuthenticated = false; } } ); }

這樣子基本上我們就可以模擬網路延遲的認證情況。

測試撰寫

在我們原本撰寫的登入測試' should show user email, logout button and without login button after authenticated. 的部份,把 spyOn 的 'isAuthenticated' 改成 'asynAuthenticate' 並把回傳值設為Observable,執行測試會發生錯誤資訊。

login.component.spec.ts
spyOn(service, 'isAuthenticated').and.returnValue(true); ..... 略 ......

改成:
spyOn(service, 'asynAuthenticated').and.returnValue( of(true).pipe(delay(3000))); ..... 略 .......

結果:

這邊會看到在測試的時候其實是抓不到使用者的 email 資訊,因為我們的認證在我們『故意』的delay 下 必須等待 3 秒才能通過認證並在之後將使用者的 email 顯示在網頁上。

測試 Async Service

這邊我們先把原本的測試 it() 加上 x 讓 Jasmine 先忽略這個測試項目並且額外撰寫一個新的 describe() 測試項目。
// ignore test when use asyn authenticated service. xit('should show user email, logout button and \ without login button after authenticated.', () => { const testEmail = 'clover@example.com'; ....... 略 ......

新的 describe() 

// async test section describe('Async testing authenticated.', () => { let component: LoginComponent; let fixture: ComponentFixture<LoginComponent>; let service: DemoService; let el: DebugElement; // 要使用它來 query dOM // 創造一個 假 的 demo service let fakeService = jasmine.createSpyObj('demo', ['asynAuthenticated']);
let spy = fakeService.asynAuthenticated.and.returnValue( of(true).pipe(delay(3000))); beforeEach(() => { TestBed.configureTestingModule({ declarations: [ LoginComponent ], imports: [ ReactiveFormsModule ], providers: [ {provide: DemoService , useValue: fakeService} ] }); fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; el = fixture.debugElement; fixture.detectChanges(); }); }};

這邊我們採用 Jasmine的 createSpyObj() 來產生一個我們之前使用 spyOn() 的物件,用他來當作我們『假的』 demo service 只不過我們目前這個假個 service 只有一個方法叫做 asynAuthenticated 名字跟我們 demo.service 裡的 'asynAuthenticated' 一模一樣

接著我們來讓這個 fakeService 的 asynAuthenticated 方法回傳一個 delay 3 秒的 true Observable 物件。 

 最後在產生測試環境的設定裡 providers 內用 fakeService 取代 DemoService 就可以得到我們的測試空間。

使用 Jasmine done function

這個 done function 是 Jasmine 內建的測試 asnyc spec,這個使用的注意地方在於我們 it() 後方的 function 原本是沒有傳入任何參數的。但是使用 done function 必須傳入 done 這個 function 參數並且在 async 測試結束地方加入 done() 來告知 Jasmine 這一段是做 async 的測試

done function

// Jasmine done function it('async testing via done: after authenticated should \ get email info , logut button and can not see login button', (done: DoneFn) => { // 重要 const testEmail = 'clover@example.com'; const testPass = 'abcd1234'; component.loginForm.patchValue({ email: testEmail, password: testPass }); component.onSubmit(); spy.calls.mostRecent().returnValue.subscribe(() => { fixture.detectChanges(); const userInfo = el.nativeElement.querySelector('#userEmail'); const loginBTN = el.nativeElement.querySelector('button[type="submit"]'); const logoutBTN = el.nativeElement.querySelector('#logoutBtn'); // 顯示使用者的 email expect(userInfo.textContent.trim()).toBe(testEmail); // 有登出按鈕 expect(logoutBTN).toBeTruthy(); // 沒有登入按鈕 expect(loginBTN).toBeNull(); done(); // 重要!別忘記加 }); });
在這額外注意的是  mostRecent() 這個 method 這個是回傳最近一次這個 spy 被使用的資訊然後透過 fixture.detectChanges() 做 data binding  再來做頁面的上面測試。


  • 另外這邊要注意的事情是由於我們的 Spy Object 回傳的是 Observable 所以最後 mostRecent().returnValue 後面是 subscribe() ,如果你的 Spy Object 回傳是 Promise 要改用 then() 來做剩下的 expect 測試。


使用 fakeAsync() 和 tick():

另一種方式為 angular 提供的 fakeAsync() 搭配 tick() 來做 async 測試。
首先我們要先 import fakeAsynctick 這兩個在 '@angular/core/teting' 的物件進來。

login.component.spec.ts
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';

接著我們撰寫一個新的 it() 做測試,程式碼如下:
login.component.spec.ts
// fakeAsync() with tick() it('async testing via fakeAsync and tick.', fakeAsync(() => { const testEmail = 'clover@example.com'; const testPass = 'abcd1234'; component.loginForm.patchValue({ email: testEmail, password: testPass }); component.onSubmit(); tick(3000); // 模擬非同步活動的時間流逝 fixture.detectChanges(); // 更新 data binding const userInfo = el.nativeElement.querySelector('#userEmail'); const loginBTN = el.nativeElement.querySelector('button[type="submit"]'); const logoutBTN = el.nativeElement.querySelector('#logoutBtn'); // 顯示使用者的 email expect(userInfo.textContent.trim()).toBe(testEmail); // 有登出按鈕 expect(logoutBTN).toBeTruthy(); // 沒有登入按鈕 expect(loginBTN).toBeNull(); }));

要注意的地方在 it() 原本後面的 function 必須被 fakeAsync() 當作參數使用
再來就是 tick() 這個 function 可以用來指定我們需要等待多少時間的流逝。例如:我們 async api 在刻意的設計下會 delay 3秒,所以就可以用 tick() 帶入 3000 ms 指定等待3秒後再做往下的測試流程。

下面這個範例可以更清楚知道 tick() 這個 function 就像時間控制器一樣:
it('should get Date diff correctly in fakeAsync', fakeAsync(() => { const start = Date.now(); tick(100); const end = Date.now(); expect(end - start).toBe(100); }));

不過這方法有個小弱點就是無法追蹤 XHR的 instance。(不過目前我還沒遇到要測試XHR的)

使用 async() with fixture.whenStable() 做非同步測試

  • 如果你是使用 angular-cli 產生專案的,這部份的設定 angular cli 會幫你設定好。如果不是請參考官方的測試設定才有辦法使用 fakeAsync() 與 async() 。

在測試環境設定中如果是使用 TestBed.compileComponents() 的方法就會在 JIT 的時候使用 XHR 來讀取外部的 template 與 css 文件,所以如果是用 compileComponents() 的測試就要用到 async() 這個 function 了。

不過這邊我的 Component with async() 測試不知道為什麼永遠都會 failed,感覺上 async() 似乎沒有真正等到 spy 給予的 3 秒後才去驗證那些 email 與按鈕,因此這邊有需要的朋友再去看看官方文件了。

........ 待續。




REF:



留言