測試命名規範與模板
為什麼測試命名很重要?
良好的測試命名不僅僅是程式碼的一部分,更是:
活文件 - 測試名稱本身就說明了功能需求
溝通工具 - 團隊成員能快速理解測試意圖
除錯指南 - 測試失敗時能立即知道問題所在
重構信心 - 清楚的測試範圍讓重構更安全
好壞命名對比
// ❌ 糟糕的測試命名
describe('Component', () => {
it('should work', () => {});
it('test functionality', () => {});
it('should do stuff correctly', () => {});
it('check if it works', () => {});
});
// ✅ 良好的測試命名
describe('SingleSelectorComponent', () => {
it('should create component successfully', () => {});
it('should emit selectionChange when option is selected', () => {});
it('should display error message when validation fails', () => {});
it('should disable form control when disabled input is true', () => {});
});
測試命名的核心原則
1. 使用 BDD (Behavior-Driven Development) 語法
格式:should [action] when [condition]
// 基本格式
it('should [預期行為] when [觸發條件]', () => {});
// 實際範例
it('should display error message when validation fails', () => {});
it('should emit selectionChange when option is selected', () => {});
it('should disable button when isLoading is true', () => {});
it('should show loading spinner when data is being fetched', () => {});
2. 描述性而非實作性
// ❌ 描述實作細節
it('should call ngOnInit method', () => {});
it('should set internal variable to true', () => {});
it('should execute private method', () => {});
// ✅ 描述行為和結果
it('should initialize component with default values', () => {});
it('should enable search functionality when searchable is true', () => {});
it('should validate user input on form submission', () => {});
3. 明確而具體
// ❌ 模糊不清
it('should handle input', () => {});
it('should work with forms', () => {});
it('should update correctly', () => {});
// ✅ 明確具體
it('should update title when title input changes', () => {});
it('should integrate correctly with reactive forms', () => {});
it('should validate email format in email field', () => {});
測試句子模板庫
預設值測試模板
should be {value} by default
測試預設值
should be false by default
should have {type} as default value
測試預設資料型別
should have empty array as default value
should default to {value}
強調預設行為
should default to 'Please select'
should initialize with {state}
測試初始狀態
should initialize with enabled state
describe('Input Properties - Default Values', () => {
it('should be false by default', () => {
expect(component.disabled()).toBe(false);
});
it('should have empty string as default value', () => {
expect(component.title()).toBe('');
});
it('should have empty array as default value', () => {
expect(component.options()).toEqual([]);
});
it('should default to Please select', () => {
expect(component.placeholder()).toBe('Please select');
});
it('should initialize with closed state', () => {
expect(component.isOpened).toBe(false);
});
});
輸入屬性測試模板
should accept input value and update {property}
測試屬性更新
should accept input value and update title
should update {property} when input changes
強調變更行為
should update disabled state when input changes
should set {property} through input properties
測試屬性設定
should set value through input properties
should bind {property} correctly
測試綁定行為
should bind formControl correctly
describe('Input Properties - Value Changes', () => {
it('should accept input value and update title', () => {
const testTitle = 'Test Title';
fixture.componentRef.setInput('title', testTitle);
fixture.detectChanges();
expect(component.title()).toBe(testTitle);
});
it('should update disabled state when input changes', () => {
fixture.componentRef.setInput('disabled', true);
fixture.detectChanges();
expect(component.disabled()).toBe(true);
});
it('should set value through input properties', () => {
const testValue = 'test-value';
fixture.componentRef.setInput('value', testValue);
fixture.detectChanges();
expect(component.value()).toBe(testValue);
});
});
顯示和渲染測試模板
should display {element} when {condition}
條件顯示
should display title when provided
should show {element} when {property} is {value}
狀態顯示
should show error when validation fails
should render {element} correctly
渲染驗證
should render options correctly
should hide {element} when {condition}
隱藏邏輯
should hide loading when data loaded
should not display {element} when {condition}
否定條件
should not display title when empty
describe('UI Rendering', () => {
it('should display title when provided', () => {
fixture.componentRef.setInput('title', 'Test Title');
fixture.detectChanges();
const titleElement = fixture.nativeElement.querySelector('.app-select-label');
expect(titleElement).not.toBeNull();
expect(titleElement.textContent).toContain('Test Title');
});
it('should show error message when validation fails', () => {
fixture.componentRef.setInput('errorMessage', 'Required field');
fixture.detectChanges();
const errorElement = fixture.nativeElement.querySelector('.error-message');
expect(errorElement).not.toBeNull();
expect(errorElement.textContent).toContain('Required field');
});
it('should not display title section when title is empty', () => {
fixture.componentRef.setInput('title', '');
fixture.detectChanges();
const titleSection = fixture.nativeElement.querySelector('.app-select-label');
expect(titleSection).toBeNull();
});
it('should render options correctly', () => {
const testOptions = [
{ value: 'option1', text: 'Option 1' },
{ value: 'option2', text: 'Option 2' }
];
fixture.componentRef.setInput('options', testOptions);
fixture.detectChanges();
expect(component.groupedOptions.length).toBeGreaterThan(0);
});
});
狀態和行為測試模板
should be {state} when {condition}
狀態驗證
should be disabled when disabled is true
should apply {state} styling when {condition}
樣式狀態
should apply error styling when invalid
should have {state} state by default
預設狀態
should have enabled state by default
should remain {state} when {condition}
狀態保持
should remain valid when input is correct
should toggle between {state1} and {state2}
狀態切換
should toggle between opened and closed
describe('Component States', () => {
it('should be disabled when disabled is true', () => {
fixture.componentRef.setInput('disabled', true);
fixture.detectChanges();
expect(component.internalFormControl.disabled).toBe(true);
});
it('should apply error styling when errorMessage is provided', () => {
fixture.componentRef.setInput('errorMessage', 'Error');
fixture.detectChanges();
const formField = fixture.nativeElement.querySelector('mat-form-field');
expect(formField.classList).toContain('--error-state');
});
it('should have enabled state by default', () => {
expect(component.disabled()).toBe(false);
expect(component.internalFormControl.enabled).toBe(true);
});
it('should toggle between opened and closed states', () => {
expect(component.isOpened).toBe(false);
component.open();
fixture.detectChanges();
expect(component.isOpened).toBe(true);
});
});
事件和輸出測試模板
should emit {event} when {action}
事件觸發
should emit selectionChange when option selected
should trigger {event} on {action}
動作觸發
should trigger validation on form submit
should call {callback} when {event}
回調調用
should call onChange when value changes
should not emit {event} when {condition}
否定事件
should not emit event when disabled
should handle {event} correctly
事件處理
should handle click events correctly
describe('Output Events', () => {
let subscription: any;
afterEach(() => {
if (subscription) {
subscription.unsubscribe();
subscription = null;
}
});
it('should emit selectionChange when value changes', () => {
const spy = jasmine.createSpy('selectionChangeSpy');
subscription = component.selectionChange.subscribe(spy);
component.internalFormControl.setValue('test-value');
expect(spy).toHaveBeenCalledWith('test-value');
});
it('should trigger opened event when dropdown opens', () => {
const spy = jasmine.createSpy('openedSpy');
subscription = component.opened.subscribe(spy);
component.open();
expect(spy).toHaveBeenCalled();
});
it('should not emit selectionChange when writeValue is called', () => {
const spy = jasmine.createSpy('selectionChangeSpy');
subscription = component.selectionChange.subscribe(spy);
component.writeValue('test-value');
expect(spy).not.toHaveBeenCalled();
});
it('should handle search input changes with debounce', fakeAsync(() => {
const spy = jasmine.createSpy('searchChangeSpy');
subscription = component.searchChange.subscribe(spy);
component.searchFilterCtrl.setValue('test');
tick(300);
expect(spy).toHaveBeenCalledWith('test');
flush();
}));
});
CVA (ControlValueAccessor) 測試模板
should implement {interface} interface
介面實作
should implement ControlValueAccessor interface
should register {callback} callback
回調註冊
should register onChange callback
should write value to internal control
值寫入
should write value to internal control
should set disabled state correctly
狀態設定
should set disabled state correctly
should integrate correctly with {system}
系統整合
should integrate correctly with FormControl
describe('CVA Mode', () => {
it('should implement ControlValueAccessor interface', () => {
expect(typeof component.writeValue).toBe('function');
expect(typeof component.registerOnChange).toBe('function');
expect(typeof component.registerOnTouched).toBe('function');
expect(typeof component.setDisabledState).toBe('function');
});
it('should write value to internal control', () => {
const testValue = 'test-value';
component.writeValue(testValue);
expect(component.internalFormControl.value).toBe(testValue);
});
it('should register onChange callback', () => {
const onChangeSpy = jasmine.createSpy('onChange');
component.registerOnChange(onChangeSpy);
component.internalFormControl.setValue('new-value');
expect(onChangeSpy).toHaveBeenCalledWith('new-value');
});
it('should set disabled state correctly', () => {
component.setDisabledState(true);
expect(component.internalFormControl.disabled).toBe(true);
component.setDisabledState(false);
expect(component.internalFormControl.enabled).toBe(true);
});
it('should integrate correctly with FormControl', () => {
const formControl = new FormControl('initial-value');
component.writeValue('initial-value');
component.registerOnChange((value) => formControl.setValue(value));
component.internalFormControl.setValue('updated-value');
expect(formControl.value).toBe('updated-value');
});
});
商業邏輯測試模板
should {action} correctly
功能執行
should filter options correctly
should handle {scenario}
場景處理
should handle empty input gracefully
should calculate {result} when {condition}
計算邏輯
should calculate total when items change
should transform {data} to {format}
資料轉換
should transform options to grouped format
should validate {input} according to {rules}
驗證邏輯
should validate email according to RFC standards
describe('Business Logic', () => {
it('should filter options correctly based on search term', () => {
const options = [
{ value: 'apple', text: 'Apple' },
{ value: 'banana', text: 'Banana' },
{ value: 'apricot', text: 'Apricot' }
];
fixture.componentRef.setInput('options', options);
component.ngOnInit();
fixture.detectChanges();
component.filterOptions('app');
component.filteredOptions.subscribe(filtered => {
expect(filtered[0].options).toHaveSize(2);
expect(filtered[0].options.map(o => o.value)).toEqual(['apple', 'apricot']);
});
});
it('should transform options to grouped format', () => {
const inputOptions = [
{ value: 'apple', text: 'Apple', group: { name: 'Fruits', order: 1 } },
{ value: 'carrot', text: 'Carrot', group: { name: 'Vegetables', order: 2 } }
];
const result = component.convertToGroupedOptions(inputOptions);
expect(result).toHaveSize(2);
expect(result[0].name).toBe('Fruits');
expect(result[1].name).toBe('Vegetables');
});
it('should handle empty input gracefully', () => {
expect(() => {
component.convertToGroupedOptions([]);
}).not.toThrow();
expect(component.convertToGroupedOptions([])).toEqual([]);
});
it('should calculate sticky header height when configuration changes', () => {
component.searchable = jasmine.createSpy().and.returnValue(true);
component.isMultipleSelector = jasmine.createSpy().and.returnValue(true);
component.isOptionGrouped = jasmine.createSpy().and.returnValue(false);
const height = component['getActualStickyHeaderHeight']();
expect(height).toBe(96); // 多選 + 搜尋 + 沒分組
});
});
錯誤處理和邊界測試模板
should handle {error_condition} gracefully
錯誤處理
should handle null input gracefully
should not throw when {condition}
異常避免
should not throw when options is undefined
should recover from {error_state}
錯誤恢復
should recover from network failure
should validate {boundary} values
邊界驗證
should validate maximum length values
should provide fallback for {scenario}
後備方案
should provide fallback for missing data
describe('Error Handling and Edge Cases', () => {
it('should handle null input gracefully', () => {
expect(() => {
fixture.componentRef.setInput('options', null);
fixture.detectChanges();
}).not.toThrow();
});
it('should not throw when title is undefined', () => {
expect(() => {
fixture.componentRef.setInput('title', undefined);
fixture.detectChanges();
}).not.toThrow();
});
it('should handle empty string values correctly', () => {
fixture.componentRef.setInput('placeholder', '');
fixture.detectChanges();
expect(component.placeholder()).toBe('');
});
it('should provide fallback for missing option text', () => {
const malformedOptions = [
{ value: 'test', text: null },
{ value: 'test2' } // 缺少 text 屬性
] as any;
expect(() => {
component.convertToGroupedOptions(malformedOptions);
}).not.toThrow();
});
it('should validate maximum option count', () => {
const largeOptions = Array.from({ length: 10000 }, (_, i) => ({
value: `option${i}`,
text: `Option ${i}`
}));
expect(() => {
fixture.componentRef.setInput('options', largeOptions);
fixture.detectChanges();
}).not.toThrow();
});
});
Directive 測試模板
should create
基本建立
should create
should apply directive without errors
套用驗證
should apply directive without errors
should handle {property} changes
屬性變更
should handle input property changes
should work with {component/library}
整合測試
should work with Angular Material
should modify {behavior} when {condition}
行為修改
should modify scroll behavior when opened
describe('MatSelectKeyboardNavigationDirective', () => {
let fixture: ComponentFixture<HostComponent>;
let host: HostComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HostComponent, NoopAnimationsModule],
}).compileComponents();
fixture = TestBed.createComponent(HostComponent);
host = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(host).toBeTruthy();
});
it('should apply directive without errors', () => {
const matSelect = fixture.nativeElement.querySelector('mat-select');
expect(matSelect).toBeTruthy();
expect(() => {
matSelect.click();
fixture.detectChanges();
}).not.toThrow();
});
it('should handle input property changes', () => {
host.searchable = true;
host.isMultiple = true;
fixture.detectChanges();
const matSelect = fixture.nativeElement.querySelector('mat-select');
expect(matSelect).toBeTruthy();
});
it('should work with Angular Material select component', () => {
const matSelect = fixture.nativeElement.querySelector('mat-select');
const matFormField = fixture.nativeElement.querySelector('mat-form-field');
expect(matSelect).toBeTruthy();
expect(matFormField).toBeTruthy();
});
});
測試結構命名規範
describe 區塊命名模板
describe('ComponentName', () => {
describe('Component Creation', () => {
it('should create', () => {});
});
describe('CVA Mode', () => {
// CVA 相關測試
});
describe('Attribute Binding Mode', () => {
// Attribute binding 相關測試
});
describe('Input Properties', () => {
describe('propertyName', () => {
it('should have correct default value', () => {});
it('should accept input and update property', () => {});
it('should display property when provided', () => {});
});
});
describe('Output Events', () => {
describe('eventName', () => {
it('should emit event when condition met', () => {});
it('should not emit event when condition not met', () => {});
});
});
describe('Public Methods', () => {
describe('methodName', () => {
it('should execute method successfully', () => {});
it('should handle edge cases in method', () => {});
});
});
describe('Business Logic', () => {
describe('featureName', () => {
it('should implement feature correctly', () => {});
});
});
describe('UI Rendering', () => {
// 渲染和視覺測試
});
describe('User Interactions', () => {
// 使用者互動測試
});
describe('Error Handling', () => {
// 錯誤處理和邊界測試
});
describe('Integration', () => {
// 整合測試
});
});
不同測試層級的命名
// 元件層級
describe('SingleSelectorComponent', () => {});
// 功能層級
describe('Single Selector - Search Functionality', () => {});
// 整合層級
describe('Single Selector - Form Integration', () => {});
// 屬性層級
describe('Input Properties', () => {
describe('title', () => {});
describe('options', () => {});
describe('disabled', () => {});
});
// 場景層級
describe('Complex Selection Scenarios', () => {
describe('when multiple options are available', () => {});
describe('when search filter is applied', () => {});
describe('when form validation is active', () => {});
});
特殊情況的命名模式
條件測試命名
describe('Conditional Behavior', () => {
describe('when user is authenticated', () => {
it('should display user profile options', () => {});
it('should enable premium features', () => {});
});
describe('when user is not authenticated', () => {
it('should display login prompt', () => {});
it('should restrict access to features', () => {});
});
describe('when feature flag is enabled', () => {
it('should show new UI components', () => {});
});
describe('when feature flag is disabled', () => {
it('should fallback to legacy UI', () => {});
});
});
異步操作命名
describe('Async Operations', () => {
it('should load data on component initialization', async () => {});
it('should show loading state while fetching data', () => {});
it('should handle network errors gracefully', () => {});
it('should retry failed requests automatically', () => {});
it('should debounce search input for 300ms', fakeAsync(() => {}));
it('should cancel previous request when new request starts', () => {});
});
效能相關測試命名
describe('Performance Considerations', () => {
it('should render large datasets efficiently', () => {});
it('should not trigger unnecessary change detection', () => {});
it('should lazy load options when needed', () => {});
it('should virtualize long lists for better performance', () => {});
it('should cache filtered results to avoid recomputation', () => {});
});
可訪問性測試命名
describe('Accessibility', () => {
it('should have proper ARIA labels', () => {});
it('should support keyboard navigation', () => {});
it('should announce selection changes to screen readers', () => {});
it('should maintain focus management correctly', () => {});
it('should provide sufficient color contrast', () => {});
});
團隊協作的命名規範
命名檢查清單
✅ 好的測試名稱特徵:
描述期望的行為或結果
包含觸發條件或前提
使用業務術語而非技術術語
長度適中(不超過一行)
語法一致(都用 should 開頭)
❌ 應避免的測試名稱:
過於技術性的描述
模糊不清的動詞
測試實作細節
過長或過短的描述
不一致的語法結構
團隊約定範例
// 團隊約定的命名模式
describe('[ComponentName]', () => {
// 基本功能測試
describe('Component Lifecycle', () => {
it('should create component successfully', () => {});
it('should initialize with correct default state', () => {});
it('should clean up resources on destroy', () => {});
});
// 功能模組測試
describe('[FeatureName] Feature', () => {
it('should [specific behavior] when [condition]', () => {});
});
// 整合測試
describe('[SystemName] Integration', () => {
it('should integrate correctly with [external system]', () => {});
});
});
多語言團隊的考量
// 如果團隊有多語言成員,保持英文測試名稱
describe('MultiLanguageComponent', () => {
it('should display content in selected language', () => {});
it('should fallback to default language when translation missing', () => {});
it('should update UI direction for RTL languages', () => {});
});
// 註解可以使用本地語言輔助理解
describe('SingleSelectorComponent', () => {
// 測試預設值設定
it('should have default placeholder text', () => {});
// 測試使用者互動
it('should respond to user selection', () => {});
// 測試邊界條件
it('should handle empty option list gracefully', () => {});
});
測試文件生成
自動化測試報告
好的測試命名可以生成有意義的測試報告:
SingleSelectorComponent
✓ should create component successfully
Input Properties
title
✓ should have empty string as default value
✓ should update title when input changes
✓ should display title when provided
options
✓ should have empty array as default value
✓ should accept input and update options
✓ should render options correctly
Output Events
selectionChange
✓ should emit selectionChange when option selected
✓ should not emit event when writeValue called
CVA Mode
✓ should implement ControlValueAccessor interface
✓ should integrate correctly with FormControl
Error Handling
✓ should handle null input gracefully
✓ should not throw when options undefined
測試覆蓋率報告
// 測試名稱直接對應功能需求
describe('Search Functionality Requirements', () => {
it('should filter options based on user input', () => {}); // REQ-001
it('should highlight matching text in results', () => {}); // REQ-002
it('should show "no results" message when no matches found', () => {}); // REQ-003
it('should clear search when user selects option', () => {}); // REQ-004
});
總結
命名規範的核心價值
可讀性 - 任何人都能理解測試意圖
可維護性 - 容易找到和修改相關測試
文件性 - 測試本身就是功能說明
除錯效率 - 快速定位問題根源
實施建議
建立團隊標準 - 統一使用測試命名模板
定期審查 - Code Review 時檢查測試命名品質
持續改進 - 根據實際使用情況調整命名規範
工具輔助 - 使用 lint 規則檢查命名一致性
長期效益
提升開發效率 - 減少理解和維護測試的時間
改善程式碼品質 - 測試驅動更好的 API 設計
增強團隊協作 - 統一的語言和理解
降低維護成本 - 清晰的測試意圖減少重寫需求
透過一致且清晰的測試命名規範,可以讓測試不僅僅是驗證程式碼正確性的工具,更成為團隊溝通和知識傳承的重要資產
Last updated