測試命名規範與模板

為什麼測試命名很重要?

良好的測試命名不僅僅是程式碼的一部分,更是:

  • 活文件 - 測試名稱本身就說明了功能需求

  • 溝通工具 - 團隊成員能快速理解測試意圖

  • 除錯指南 - 測試失敗時能立即知道問題所在

  • 重構信心 - 清楚的測試範圍讓重構更安全

好壞命名對比

// ❌ 糟糕的測試命名
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
});

總結

命名規範的核心價值

  • 可讀性 - 任何人都能理解測試意圖

  • 可維護性 - 容易找到和修改相關測試

  • 文件性 - 測試本身就是功能說明

  • 除錯效率 - 快速定位問題根源

實施建議

  1. 建立團隊標準 - 統一使用測試命名模板

  2. 定期審查 - Code Review 時檢查測試命名品質

  3. 持續改進 - 根據實際使用情況調整命名規範

  4. 工具輔助 - 使用 lint 規則檢查命名一致性

長期效益

  • 提升開發效率 - 減少理解和維護測試的時間

  • 改善程式碼品質 - 測試驅動更好的 API 設計

  • 增強團隊協作 - 統一的語言和理解

  • 降低維護成本 - 清晰的測試意圖減少重寫需求

透過一致且清晰的測試命名規範,可以讓測試不僅僅是驗證程式碼正確性的工具,更成為團隊溝通和知識傳承的重要資產

Last updated