測試中的非同步操作處理
在 Angular 應用程式測試中,我們經常需要處理非同步操作,例如動態載入的元件、API 呼叫、動畫等。特別是在測試使用者互動後的 UI 更新時,正確處理這些非同步操作變得尤為重要。
測試中的非同步挑戰
在 Angular 中進行測試時,我們可能會遇到以下非同步操作:
HTTP 請求: 向後端 API 發送請求並等待回應
定時器操作: setTimeout、setInterval 等
動畫: Angular 的動畫完成需要時間
視圖更新: 變更檢測和渲染需要時間
動態載入: 某些元件或內容可能在使用者互動後才顯示
這些非同步操作如果不妥善處理,會導致測試不穩定,可能出現「有時通過、有時失敗」的情況,極大地降低測試的可靠性。
async/await 在測試中的應用
async/await
是 JavaScript 中處理非同步操作的現代語法,在測試中使用它有很多優勢:
基本語法
it('應該完成非同步操作', async () => {
// 準備階段
// 執行階段 - 包含非同步操作
await someAsyncOperation();
// 斷言階段
expect(result).toBe(expectedValue);
});
在 Angular 測試中的應用
在 Angular 測試中,我們經常需要使用 fixture.whenStable()
方法等待所有非同步操作完成:
it('應該在非同步操作後更新視圖', async () => {
// 執行可能導致非同步操作的動作
component.loadData();
fixture.detectChanges();
// 等待所有非同步操作完成
await fixture.whenStable();
// 此時視圖已更新,可以進行斷言
const element = fixture.nativeElement.querySelector('.data-item');
expect(element).toBeTruthy();
});
fixture.whenStable() 的作用
fixture.whenStable()
返回一個 Promise,該 Promise 會在所有非同步任務(包括 setTimeout、API 呼叫、Promise 等)完成後解析(resolve)。這使我們能夠確保在進行斷言時,所有變更已經完成並且視圖已更新。
動態載入元素的測試技巧
某些 UI 元素可能不是一開始就存在於 DOM 中,而是在特定條件下動態載入的。這些元素的測試需要特別的技巧:
1. 模擬觸發條件
首先需要模擬會導致元素出現的條件,例如點擊事件:
// 獲取觸發元素
const triggerElement = fixture.debugElement.query(By.css('.trigger-button')).nativeElement;
// 模擬點擊
triggerElement.click();
fixture.detectChanges();
2. 等待元素載入
使用 await fixture.whenStable()
等待動態載入完成:
// 等待載入完成
await fixture.whenStable();
fixture.detectChanges(); // 再次觸發變更檢測
3. 使用正確的選擇器
動態載入的元素可能位於不同的 DOM 層級,甚至可能被放置在應用的根層級而不是元件內部:
// 錯誤:僅在元件內部查詢
const element = fixture.nativeElement.querySelector('.dynamic-element');
// 正確:在整個文檔中查詢
const element = document.querySelector('.dynamic-element');
測試案例:測試動態搜尋框
以下是一個實際測試動態加載搜尋框的例子。在這個例子中,搜尋框在點擊選擇器(mat-select)後才會顯示:
it('searchable 設置為 true 應收到 true 且點擊後顯示搜尋框', async () => {
// 設置輸入參數
fixture.componentRef.setInput('searchable', true);
fixture.detectChanges();
// 模擬點擊選擇器以顯示下拉選單
const selectElement = fixture.elementRef.nativeElement.querySelector('mat-select');
selectElement.click();
fixture.detectChanges();
// 等待面板渲染完成
await fixture.whenStable();
// 檢查搜尋框是否存在
// 注意:搜尋框可能在文檔的其他部分,而不是在元件內部
const searchElement = document.querySelector('.mat-select-search-input');
// 斷言
expect(component.searchable()).toBe(true);
expect(searchElement).toBeTruthy();
});
關鍵要點解析
使用 async 函數:允許我們在測試中使用 await 關鍵字
触發實際行為:模擬點擊 mat-select 觸發下拉選單顯示
等待渲染完成:使用 await fixture.whenStable() 確保所有非同步操作完成
正確選擇查詢範圍:使用 document.querySelector 在整個文檔範圍內查詢
多重檢查:同時檢查 component 屬性和 DOM 元素
其他處理非同步的方法
除了 async/await 之外,Angular 測試還提供了其他處理非同步操作的方法:
1. fakeAsync 和 tick
import { fakeAsync, tick } from '@angular/core/testing';
it('應該在延遲後顯示訊息', fakeAsync(() => {
component.showMessageAfterDelay();
// 推進時間
tick(1000);
fixture.detectChanges();
const message = fixture.nativeElement.querySelector('.message');
expect(message.textContent).toContain('延遲訊息');
}));
fakeAsync
允許在測試中控制時間的流逝,而 tick()
用於推進虛擬時鐘。
2. 使用 Promise.then()
如果不想使用 async/await,可以使用 Promise 鏈式寫法:
it('應該在非同步操作後更新視圖', () => {
component.loadData();
fixture.detectChanges();
return fixture.whenStable().then(() => {
const element = fixture.nativeElement.querySelector('.data-item');
expect(element).toBeTruthy();
});
});
3. done 回調(較舊的方式)
it('應該在回調後更新視圖', (done) => {
component.loadData(() => {
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('.data-item');
expect(element).toBeTruthy();
done();
});
});
總結
使用 async/await 是處理 Angular 測試中非同步操作的現代且推薦的方式
fixture.whenStable() 對於等待各種非同步操作完成非常有用
動態載入元素 需要特別處理,包括正確觸發其顯示條件和適當等待
選擇正確的 DOM 查詢範圍 對於測試動態加載的元素至關重要
同時測試組件屬性和 DOM 狀態 可以提供更全面的測試覆蓋
通過妥善處理非同步操作,我們可以建立更可靠、更穩定的測試,進而提高整個應用程式的品質。
Last updated