iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0

今天要透過上一篇介紹的單元測試撰寫方式,實際來為一個 component 撰寫單元測試。
使用Jest為Angular專案撰寫UnitTest(一)
使用Jest為Angular專案撰寫UnitTest(二)

廢話不多說,上程式碼!

import { Component} from '@angular/core';
import { NgIf, NgFor, NgClass } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TodoService } from './todo.service';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

@Component({
  selector: 'app-todo',
  standalone: true,
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css'],
  imports: [NgIf, NgFor, NgClass, FormsModule],
})
export class TodoComponent {
  todos: Todo[] = [];
  errorMessage: string = '';

  constructor(private todoService: TodoService) {}

  loadTodos(): void {
    this.todoService.getTodos().subscribe(
      (todos) => (this.todos = todos),
      (error) => (this.errorMessage = 'Failed to load todos')
    );
  }
}

這是一個關於 Todo List 的簡單 component
其中注入了一個 service ,並且有一個 method


單元測試實作

  1. 首先我們建立一支測試檔案 todo.component.spec.ts,記得 .spec prefix 副檔名。
  2. 在檔案中加入描述 TodoComponent 的第一層 describe
describe('TodoComponent', () => {})
  1. 在檔案中加入初始設定。
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TodoComponent } from './todo.component';

describe('TodoComponent', () => {
  let component: TodoComponent;
  let fixture: ComponentFixture<TodoComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [TodoComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(TodoComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
});

beforeEach 在每一次執行測案前,執行其中的程式碼。

TestBed.configureTestingModule 是Angular提供給我們設定測試模組用的方法。像是測試的 app.config 檔案的概念。我們會在其中匯入各種需要用到的模組和注入服務。

compileComponents 是編譯 component 的非同步方法,確保測試前所有資源已經編譯完成。

component 代表我們要在測試期間使用的 TodoComponent 的實例。

fixture 代表 ComponentFixture 的實例,讓我們可以訪問 template 的部分。

detectChanges 會手動觸發 Angular 的變更檢測機制,以觸發元件生命週期。

  1. 隔離外部服務,我們需要確保測試檔案式獨立不會被外部因素影響。可以看到 TodoComponent 中注入了 TodoService,所以我們要透過 mock 的方式來隔離。
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TodoComponent } from './todo.component';
import { TodoService } from './todo.service';

describe('TodoComponent', () => {
  let component: TodoComponent;
  let fixture: ComponentFixture<TodoComponent>;
  let mockTodoService: any;
  const mockTodos = [
    { id: 1, title: 'Learn Angular', completed: false },
    { id: 2, title: 'Write Jest Tests', completed: false },
  ];

  beforeEach(async () => {
    mockTodoService = {
      getTodos: jest.fn().mockReturnValue(of(mockTodos))
    };

    await TestBed.configureTestingModule({
      imports: [TodoComponent],
      providers: [{ provide: TodoService, useValue: mockTodoService }]
    }).compileComponents();

    fixture = TestBed.createComponent(TodoComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
});

首先宣告仿造的 service ,並在 provide 的階段使用仿造 service 來進行注入。

  1. 加入驗證 component 是否成功渲染的防呆驗證。
  it('should create the component', () => {
    expect(component).toBeTruthy();
  });
  1. 針對 component 中的每一個 function 建立相符的 describe
describe('loadTodos()', () => {
});
  1. 發想測試情境,針對函數中的正常系和異常系進行測試案例設計,確保每一個 it 只完成一個驗證,以及不相互耦合。

    loadTodos() 為範例,可以看到,串流本身依據服務的回傳分成正常和異常處理,要涵蓋這兩個情境基本就需要兩個 it

  describe('loadTodos()', () => {
    it('should load todos successfully', () => {})
    it('should handle error when loading todos', () => {});
  });
  1. 使用 AAA Pattern 撰寫測試。should load todos successfully 因為我們在 beforeEach 中已經宣告了正常系的假資料,所以 Arrange 中不需要再次定義。在Act中直接執行 loadTodos() 。最後在 Assert 中 撰寫期望的結果。
it('should load todos successfully', () => {
  // 正常情境測試
  // Arrange
	// Act
  component.loadTodos();
  // Assert
  expect(component.todos.length).toBe(2);
  expect(component.todos[0].title).toBe(mockTodos[0].title);
});

接著在異常系測試中,我們就需要調整 service 回傳的內容,使其觸發異常系的流程。Arrange階段我們可以利用 jest.spyOn 來監聽,並修改回傳內容。

it('should handle error when loading todos', () => {
	// Arrange
  // 使用 spyOn 來模擬 TodoService 的 getTodos 方法並返回錯誤
  const todoServiceSpy = jest.spyOn((component as any)['todoService'] , 'getTodos').mockReturnValue(throwError(() => new Error('Failed to load')));
  
  // Act
  component.loadTodos(); // 觸發 loadTodos
  
  // Assert
  expect(component.errorMessage).toBe('Failed to load todos');
  expect(todoServiceSpy).toHaveBeenCalled();
});

值得一提的是,因為 todoService 在component中是 private 的,我們可以透過 as any 的方式使之繞過型別檢核,讓我們能在外部取得 service

  1. 在每個測案後進行 mock 的還原。確保測案之間的資料狀態獨立。
afterEach(() => {
  jest.clearAllMocks(); // 每次測試後清除所有的 mock 狀態
});
  1. 執行測試,透過 npm 中設定好的 script 執行 jest 的測試。
npm run test-jest:coverage

測試涵蓋率

最後補充一下,在單元測試中,我們經常使用四個主要指標來衡量測試的質量、覆蓋範圍以及系統的健全性。以確保確保測試的有效性和完整性,並且幫助開發者找出測試中的不足之處。分別是:

1. 測試覆蓋率(Test Coverage)

測試覆蓋率 是指測試用例覆蓋代碼的比例,通常會分為以下幾種覆蓋率:

  • 行覆蓋率(Line Coverage):測量測試執行時,實際執行的代碼行數相對於總行數的比例。這是最直觀的覆蓋率指標。
  • 函數覆蓋率(Function Coverage):檢查測試是否執行了每個函數或方法。
  • 分支覆蓋率(Branch Coverage):測試是否涵蓋了所有條件分支(如 ifswitch),確保每個邏輯分支都被測試過。
  • 路徑覆蓋率(Path Coverage):測試是否執行了代碼中所有可能的執行路徑。

測試覆蓋率的重要性

  • 測試覆蓋率高表明更多的代碼行被測試,減少了遺漏 bug 的可能性。
  • 但高覆蓋率並不一定等於高質量的測試,因為覆蓋率並不衡量測試的深度或質量,只能確保代碼在測試時有被執行。

2. 測試通過率(Test Pass Rate)

https://ithelp.ithome.com.tw/upload/images/20241012/20169487itKNNwm1FV.png

基本上請確保測試通過率是100%。

3. 測試執行速度(Test Execution Time)

測試執行速度 是指執行測試所花費的時間。這是一個非常重要的指標,特別是在 CI/CD (持續集成/持續交付)環境中,測試過程的效率直接影響整體開發流程的效率。

  • 單個測試用例的執行時間:確保每個測試用例運行時間短,避免測試過程過於耗時。
  • 整體測試套件的執行時間:隨著測試數量的增加,整個測試套件的執行時間可能會變長,需要持續關注這個指標。

測試執行速度的重要性

  • 測試執行時間越短,開發過程中的反饋循環就越快,開發者能夠更快地發現問題。
  • 如果測試過程過於緩慢,可能會導致開發者減少執行測試的頻率,進而降低測試的價值。

如何提高測試執行速度

  • 減少依賴於外部服務的測試(例如,使用 mock 來取代實際的 API 調用)。
  • 優化測試策略,確保每個測試是獨立且快速執行的。

4. 測試穩定性(Test Stability)

測試穩定性 是指測試的結果是否一致、可預測。穩定的測試應該每次運行結果都一致,即使在不同的環境下執行也應該得到相同的結果。

  • 不穩定測試(Flaky Tests):這些測試有時會通過,有時會失敗,通常是由於測試環境的不確定性、隨機因素、競態條件等引起的。
  • 穩定測試:測試的結果應該是確定的,無論在何種環境下運行都應該給出一致的結果。

測試穩定性的重要性

  • 測試的不穩定性會導致測試結果不可靠,讓開發者難以分辨是代碼問題還是測試本身的問題。
  • 稳定的測試可以提高開發者對測試結果的信心,有助於發現真實的問題。

如何提高測試穩定性

  • 減少測試對外部依賴(如網絡、文件系統、時鐘等),可以使用 mock 和 stub 來模擬這些外部依賴。

以上就是我們的單元測試分享,大家也可以依照自己的專案狀況來衡量撰寫單元測試時的目標設定,如果是時間緊迫人數又少,也可以先針對重要的檔案撰寫,並且涵蓋率可以設定的較低一些,未來在逐漸補齊也是沒有問題的,最重要的是將測試的撰寫加入開發流程中,確保持續的維護。


上一篇
# 使用Jest為Angular專案撰寫UnitTest(二)
下一篇
# 使用Cypress為Angular專案撰寫整合測試特性的E2E測試
系列文
轉生成前端工程師後,30步離開新手村!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言