測試中的非同步操作處理

在 Angular 應用程式測試中,我們經常需要處理非同步操作,例如動態載入的元件、API 呼叫、動畫等。特別是在測試使用者互動後的 UI 更新時,正確處理這些非同步操作變得尤為重要。

測試中的非同步挑戰

在 Angular 中進行測試時,我們可能會遇到以下非同步操作:

  1. HTTP 請求: 向後端 API 發送請求並等待回應

  2. 定時器操作: setTimeout、setInterval 等

  3. 動畫: Angular 的動畫完成需要時間

  4. 視圖更新: 變更檢測和渲染需要時間

  5. 動態載入: 某些元件或內容可能在使用者互動後才顯示

這些非同步操作如果不妥善處理,會導致測試不穩定,可能出現「有時通過、有時失敗」的情況,極大地降低測試的可靠性。

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();
});

關鍵要點解析

  1. 使用 async 函數:允許我們在測試中使用 await 關鍵字

  2. 触發實際行為:模擬點擊 mat-select 觸發下拉選單顯示

  3. 等待渲染完成:使用 await fixture.whenStable() 確保所有非同步操作完成

  4. 正確選擇查詢範圍:使用 document.querySelector 在整個文檔範圍內查詢

  5. 多重檢查:同時檢查 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();
  });
});

總結

  1. 使用 async/await 是處理 Angular 測試中非同步操作的現代且推薦的方式

  2. fixture.whenStable() 對於等待各種非同步操作完成非常有用

  3. 動態載入元素 需要特別處理,包括正確觸發其顯示條件和適當等待

  4. 選擇正確的 DOM 查詢範圍 對於測試動態加載的元素至關重要

  5. 同時測試組件屬性和 DOM 狀態 可以提供更全面的測試覆蓋

通過妥善處理非同步操作,我們可以建立更可靠、更穩定的測試,進而提高整個應用程式的品質。

Last updated