Angular 17 測試基礎
為什麼需要測試?
在 Angular 開發中,測試是確保程式碼品質和穩定性的重要手段。特別是在使用 Angular 17 的 standalone 元件和新的 control flow 語法(@for
、@if
等)時,良好的測試策略更顯重要。
測試的價值
防止回歸錯誤 - 確保新功能不會破壞既有功能
程式碼品質保證 - 強制思考程式碼的設計和邊界條件
重構信心 - 安全地改善程式碼結構
文件化功能 - 測試本身就是最好的使用說明
Angular 測試環境設定
基本匯入設定
在 Angular 17 standalone 架構下,測試設定需要特別注意模組的匯入:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
標準測試配置模板
describe('YourComponent', () => {
let component: YourComponent;
let fixture: ComponentFixture<YourComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
YourComponent, // Standalone 元件直接匯入
NoopAnimationsModule, // 禁用動畫提升測試效能
ReactiveFormsModule, // 表單相關功能
MatFormFieldModule, // Angular Material 模組
MatSelectModule,
// 其他必要模組...
],
}).compileComponents();
fixture = TestBed.createComponent(YourComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Angular 17 Standalone 測試特點
與傳統模組化差異
傳統模組化方式:
// ❌ 舊的方式
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [YourComponent], // 使用 declarations
imports: [CommonModule, FormsModule],
}).compileComponents();
});
Standalone 方式:
// ✅ 新的方式
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [YourComponent], // 直接匯入 standalone 元件
}).compileComponents();
});
Control Flow 語法測試
Angular 17 的新 control flow 語法(@if
、@for
、@switch
)在測試中的處理:
// 元件 template 使用新語法
template: `
@if (showTitle) {
<h1>{{ title }}</h1>
}
@for (option of options; track option.value) {
<div>{{ option.text }}</div>
}
`
// 測試新 control flow
it('should display title when showTitle is true', () => {
component.showTitle = true;
component.title = 'Test Title';
fixture.detectChanges();
const titleElement = fixture.nativeElement.querySelector('h1');
expect(titleElement).not.toBeNull();
expect(titleElement.textContent).toBe('Test Title');
});
it('should render options using @for', () => {
component.options = [
{ value: '1', text: 'Option 1' },
{ value: '2', text: 'Option 2' }
];
fixture.detectChanges();
const optionElements = fixture.nativeElement.querySelectorAll('div');
expect(optionElements.length).toBe(2);
expect(optionElements[0].textContent).toBe('Option 1');
});
測試工具與函式庫
Jasmine 基本語法
Angular 使用 Jasmine 作為測試框架,提供豐富的斷言方法:
// 基本斷言
expect(value).toBe(expected); // 嚴格相等 (===)
expect(value).toEqual(expected); // 深度相等
expect(value).toBeTruthy(); // 真值
expect(value).toBeFalsy(); // 假值
expect(value).toBeNull(); // null
expect(value).toBeUndefined(); // undefined
expect(value).toBeDefined(); // 已定義
// 陣列和物件
expect(array).toContain(item); // 包含元素
expect(array).toHaveSize(number); // 陣列長度
expect(object).toHaveProperty('key'); // 物件屬性
// 數字比較
expect(number).toBeGreaterThan(other);
expect(number).toBeLessThan(other);
expect(number).toBeCloseTo(other, precision);
// 字串
expect(string).toContain(substring);
expect(string).toMatch(/pattern/);
// 函式
expect(fn).toThrow();
expect(fn).toThrowError(ErrorType);
Spy 函式
Spy 是測試中監控和模擬函式調用的重要工具:
// 建立 spy
const spy = jasmine.createSpy('spyName');
const methodSpy = spyOn(object, 'methodName');
// Spy 驗證
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(arg1, arg2);
expect(spy).toHaveBeenCalledTimes(number);
// Spy 回傳值設定
spy.and.returnValue(value);
spy.and.callThrough(); // 調用原始函式
spy.and.throwError(error); // 拋出錯誤
異步測試處理
fakeAsync 和 tick
處理有時間延遲的操作:
import { fakeAsync, tick, flush } from '@angular/core/testing';
it('should handle debounced input', fakeAsync(() => {
component.searchInput.setValue('test');
// 模擬時間經過
tick(300); // 等待 300ms
expect(component.searchResults).toBeDefined();
// 清理所有剩餘的計時器
flush();
}));
async 和 whenStable
處理 Promise 和其他異步操作:
it('should load data asynchronously', async () => {
component.loadData();
fixture.detectChanges();
// 等待所有異步操作完成
await fixture.whenStable();
expect(component.data.length).toBeGreaterThan(0);
});
常用測試配置
禁用動畫
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
// 在 TestBed 中加入
imports: [NoopAnimationsModule]
Mock 服務
const mockService = {
getData: jasmine.createSpy('getData').and.returnValue(of(testData)),
saveData: jasmine.createSpy('saveData').and.returnValue(of({}))
};
beforeEach(() => {
TestBed.configureTestingModule({
// ...
providers: [
{ provide: YourService, useValue: mockService }
]
});
});
處理 Icon 錯誤
Angular Material 的 icon 在測試環境中常見問題:
import { MatIconRegistry } from '@angular/material/icon';
import { of } from 'rxjs';
providers: [
{
provide: MatIconRegistry,
useValue: {
addSvgIcon: () => {},
getNamedSvgIcon: () =>
of(document.createElementNS('http://www.w3.org/2000/svg', 'svg')),
},
},
],
測試最佳實踐
1. AAA 模式
Arrange - Act - Assert 是測試的標準結構:
it('should update title when input changes', () => {
// Arrange - 準備測試資料
const testTitle = 'Test Title';
// Act - 執行操作
fixture.componentRef.setInput('title', testTitle);
fixture.detectChanges();
// Assert - 驗證結果
expect(component.title()).toBe(testTitle);
});
2. 一個測試一個功能
// ✅ 好的做法 - 每個測試專注於一個功能
it('should have empty string as default value', () => {
expect(component.title()).toBe('');
});
it('should update title when input changes', () => {
fixture.componentRef.setInput('title', 'Test');
fixture.detectChanges();
expect(component.title()).toBe('Test');
});
// ❌ 避免的做法 - 一個測試包含多個驗證
it('should handle title correctly', () => {
expect(component.title()).toBe('');
fixture.componentRef.setInput('title', 'Test');
fixture.detectChanges();
expect(component.title()).toBe('Test');
// 太多功能混在一起
});
3. 有意義的測試描述
// ✅ 清楚的描述
it('should display error message when validation fails', () => {});
it('should emit selectionChange when option is selected', () => {});
// ❌ 模糊的描述
it('should work correctly', () => {});
it('should test functionality', () => {});
4. 適當的測試覆蓋率
重點測試:
公開 API - 輸入屬性、輸出事件、公開方法
商業邏輯 - 核心功能和計算
邊界條件 - 空值、極限值、錯誤情況
使用者互動 - 點擊、輸入、導航
不需要測試:
私有方法 - 透過公開方法間接測試
第三方程式庫 - 假設它們已經經過測試
簡單的 getter/setter - 除非有特殊邏輯
常見錯誤與解決
1. 忘記 detectChanges()
// ❌ 錯誤 - 忘記觸發變更檢測
it('should display title', () => {
component.title = 'Test';
// 忘記 fixture.detectChanges();
const element = fixture.nativeElement.querySelector('.title');
expect(element.textContent).toBe('Test'); // 可能失敗
});
// ✅ 正確
it('should display title', () => {
component.title = 'Test';
fixture.detectChanges(); // 觸發變更檢測
const element = fixture.nativeElement.querySelector('.title');
expect(element.textContent).toBe('Test');
});
2. 異步操作處理不當
// ❌ 錯誤 - 沒有等待異步操作
it('should load data', () => {
component.loadData();
expect(component.data).toBeDefined(); // 可能失敗
});
// ✅ 正確
it('should load data', async () => {
component.loadData();
fixture.detectChanges();
await fixture.whenStable();
expect(component.data).toBeDefined();
});
3. 資源清理
describe('Component with subscriptions', () => {
let subscription: any;
afterEach(() => {
// 清理訂閱避免記憶體洩漏
if (subscription) {
subscription.unsubscribe();
subscription = null;
}
});
it('should handle events', () => {
subscription = component.eventEmitter.subscribe(spy);
// 測試邏輯...
});
});
總結
Angular 17 的測試基礎包括:
環境設定 - Standalone 元件的匯入方式
測試工具 - Jasmine、Spy、異步處理
最佳實踐 - AAA 模式、單一功能測試、有意義的描述
常見問題 - 變更檢測、異步操作、資源清理
掌握這些基礎概念後,就可以進入更進階的 ComponentFixture 和 Host Component 測試技巧。
Last updated