iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 29
0
Modern Web

Angular 全集中筆記系列 第 29

第 29 型 - 單元測試 (Unit Testing)

  • 分享至 

  • xImage
  •  

在應用程式開發中,常會利用人工手動去測試系統的正確,不過當遇到較複雜的系統時,手動測試相對耗時且容易遺漏。除此之外,也可以撰寫單元測試 (Unit Testing) 程式並利用測試框架運行,來驗證系統的正確性;此通常針對的是方法或是類別,透過封裝隔離來縮小測試的範圍,以在持續開發與維護中確保程式品質。這一篇將了解在 Angular 應用程式如何針對元件進行單元測試 (Unit Test)。

Jasmine

Angular 官方建議使用 Jasmine 來撰寫測試程式,將測試程式撰寫在 *.spec.ts 檔案中,透過下列幾個方法來定義各種的測試案例。

describe('測試分組描述', () => {
  beforeAll(() => { });

  afterAll(() => { });

  beforeEach(() => { });

  it('測試案例描述', () => { });

  afterEach(() => { });
});

每一個測試案例皆會寫在 it() 方法之中,且每一個案例皆不互相影響;當測試案例愈來愈多時,可以利用 describe() 方法來分組,此方法可以有一至多個 describe()it() 方法。在執行 describe 下的所有測試案例之前,會觸發一次 beforeAll() 方法,此方法用於初始化所有案例所需的物件;當所有測試案例完成後,則會觸發一次 afterAll() 方法來注銷全域物件。而在每一個案例在執行的前後則分別會觸發 beforeEach()afterEach() 方法。另外,可以在 describe()it() 前加上 f,來只執行特定的測試案例;或是加上 x 來排除不執行該測試案例。

Angular 測試環境

在執行測試時,Angular 會透過 TestBed 動態建立用來模擬 @NgModule 的測試模組,因此會在此針對所測試的元件對象匯入所需要的模組或元件。

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [TaskComponent]
    })
    .compileComponents();
  }));

利用單元測試驗證待辦事項主旨的顯示正確性

大致了解 Jasmine 的方法後,首先在 task.component.spec.ts 中新增驗證主旨顯示的測試案例。

describe('TaskComponent', () => {
  it('當指定主旨為 "頁面需要顯示待辦事項主旨", 頁面應顯示為 "頁面需要顯示待辦事項..."', () => {
  });
});

在撰寫單元測試程式時會採用 3A 原則來加強測試程式的可讀性;此原則包含了:

  • Arrange - 初始化所需要的資訊,或預期的結果
  • Act - 實際執行所要測試的對象
  • Assert - 驗證執行的結果是否符合預期
describe('TaskComponent', () => {
  it('當指定主旨為 "頁面需要顯示待辦事項主旨", 頁面應顯示為 "頁面需要顯示待辦事項..."', () => {
    // arrange
    const expected = '頁面需要顯示待辦事項...';

    // act

    // assert
  });
});

另外,在對元件進行測試時,Angular 會利用 TestBed 建立元件的實體,並提供 ComponentFixture 物件來對元件與其 DOM 進行互動。因此此測試案例會針對 fixture 物件的元件實體變數 component 設定主旨屬性,並呼叫 detectChanges() 方法讓 TestBed 執行資料繫結。

describe('TaskComponent', () => {
  it('當指定主旨為 "頁面需要顯示待辦事項主旨", 頁面應顯示為 "頁面需要顯示待辦事項..."', () => {
    // arrange
    const expected = '頁面需要顯示待辦事項...';

	// act
    component.subject = '頁面需要顯示待辦事項主旨';
    fixture.detectChanges();

    // assert
  });
});

最後需要驗證頁面顯示的主旨是否符合預期,可以查詢 ComponentFixture 內的 DebugElement 樹來取得到 DebugElement 節點,進一步使用 nativeElement 來取得執行平台的原生物件,而指定的方式可以利用 css 選擇器或是直接指定元件類型。然後利用 Jasmine 提供的方法進行驗證後,就可以執行 ng test 來運行測試程式了。

describe('TaskComponent', () => {
  it('當指定主旨為 "頁面需要顯示待辦事項主旨", 頁面應顯示為 "頁面需要顯示待辦事項..."', () => {
    // arrange
    const expected = '頁面需要顯示待辦事項...';

	// act
    component.subject = '頁面需要顯示待辦事項主旨';
    fixture.detectChanges();

	// assert
    const debugElement = fixture.debugElement.query(By.css('div.content span'));
    expect(debugElement.nativeElement.textContent).toContain(expected);
  });
});

Testing

驗證待辦事項清單顯示個數是否正確

當元件有注入服務時,所撰寫單元測試一般不會直接使用實際的服務,而是建立一個 Spy 服務物件來模擬,如此才可以控制元件的輸入與輸出的狀態。不過,因為在 TaskList 元件中注入了 Router 服務元件,並且有使用到 Tsak 元件,所以在 task-list.component.spec.ts 中需要先匯入所需要的模組與元件。

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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [RouterTestingModule, HttpClientModule, FormsModule],
      declarations: [TaskListComponent, TaskComponent, TaskStateColorDirective],
    }).compileComponents();
  }));
});

接著,利用 Jasmine 提供的 createSpyObj 來建立一個待辦事項服務的 Spy 物件,且定義此服務元件 getData() 所回傳的值,並注入在 TestBed 內以取代實際的待辦事項服務。

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

  let taskService: jasmine.SpyObj<TaskRemoteService>;

  beforeEach(async(() => {
    taskService = jasmine.createSpyObj(['getData']);
    taskService.getData.and.returnValue(
      of([
        {
          id: 1,
          subject: '頁面需要顯示待辦事項主旨',
          state: 0,
          level: 'XS',
          tags: ['FEATURE', 'ISSUE', 'enhancement', 'discussion'],
        },
      ])
    );

    TestBed.configureTestingModule({
      imports: [RouterTestingModule, HttpClientModule, FormsModule],
      declarations: [TaskListComponent, TaskComponent, TaskStateColorDirective],
      providers: [{ provide: TaskRemoteService, useValue: taskService }],
    }).compileComponents();
  }));
});

最後建立一個測試案例,直接利用 TaskComponent 來查詢頁面上所有 DebugElement 節點,並驗證其個數是否符合服務所設定的個數。

  fit('應顯示一筆待辦事項', () => {
    const debugElements = fixture.debugElement.queryAll(By.directive(TaskComponent));
    expect(debugElements.length).toBe(1);
  });

結論

利用單元測試可以將測試案例記錄下,在持續的開發與維護中重覆的執行,減少人工測試的時間成本,更進一步的搭配 CI 來避免把有問題的程式更新至正式環境中。


上一篇
第 28 型 - 路由 (Router) - Resolve / 延遲載入 (Lazy Router)
下一篇
第 30 型 - 環境配置與建構 (Build)
系列文
Angular 全集中筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言