spyOn() 的使用心得整理

spyOn() 是整合測試與單元測試的關鍵,本文深入探討 spyOn() 用於整合測試部分。

Version


Angular CLI 1.1.2
Angular 4.2.3

定義


spyOn000

  • 驗收測試:以 HTML 與 CSS 的角度測試最後結果,配合真資料測試,與 Angular 無關
  • 整合測試:以 Angular 角度測試元件與元件之間的關係,配合假資料測試,與 Angular 有關
  • 單元測試:以 class 角度測試,配合假資料測試,與 Angular 無關

最簡單的是單元測試,常常一個 expect() 就可搞定

其次是驗收測試,只要會寫 CSS selector 也不難

最難寫的是整合測試,要對相依物件加以假設並傳入假資料與驗證假資料

測試目的


Q:驗收測試、整合測試與單元測試到底在測什麼?

  • 驗收測試:測最終結果
  • 整合測試:測之間關係
  • 單元測試:測單一結果

整合測試


測試案例:<h1></h1> 內應該使用 getTitle() 函式

1
2
3
4
5
6
7
it(`should use getTitle() in HTML`, () => {
component.getTitle = () => 'fake';
fixture.detectChanges();

htmlElement = debugElement.query(By.css('h1')).nativeElement;
expect(htmlElement.textContent).toBe('fake');
});

在測試 <h1></h1> 內應使用 title field 時,我們曾經指定 title 為假資料 fake,並測試 <h1></h1> 內資料是否為 fake

同樣的技巧,我們也可以指定 component.getTitle = () => 'fake',並測試 <h1></h1> 內資料是否為 fake

將相依物件設定為假資料,並測試目前物件是否有相依物件的假資料,為整合測試的基本招式

SpyOn()


以上方式雖然可行,算土法煉鋼,Jasmine 另外提供更方便的方式:spyOn()

Spy 英文為間諜,顧名思義,間諜就是假的,就是幫我們建立假物件、假資料。

spyOn001

若使用 spyOn()spyOn() 會對原本的 getTitle() 進行攔胡,HTML 將無法去呼叫原本 TitleComponent.getTitle(),而改呼叫 Spy.getTitle()

spyOn() 另外一個特點,就是對不存在的相依物件進行 spyOn() 時,會測試紅燈,可藉此驅動出相依物件的單元測試,這也是 ATDD 能 out side in 的關鍵。

callFake()

1
2
3
4
5
6
7
8
9
it(`should use getTitle() in HTML`, () => {
spyOn(component, 'getTitle').and.callFake(() => {
return 'fake';
});
fixture.detectChanges();

htmlElement = debugElement.query(By.css('h1')).nativeElement;
expect(htmlElement.textContent).toBe('fake');
});

callFake() 會傳入 arrow function 回傳假資料,用來取代想 spyOn() 的 function。

最後 expect() 一樣測試假資料

returnValue()

1
2
3
4
5
6
7
it(`should use getTitle() in HTML`, () => {
spyOn(component, 'getTitle').and.returnValue('fake');
fixture.detectChanges();

htmlElement = debugElement.query(By.css('h1')).nativeElement;
expect(htmlElement.textContent).toBe('fake');
});

直接使用 returnValue() 回傳假資料,寫法比 callFake() 簡潔。

最後 expect() 一樣測試假資料

calls.any()

1
2
3
4
5
6
it(`should use getTitle() in HTML`, () => {
const spy = spyOn(component, 'getTitle');
fixture.detectChanges();

expect(spy.calls.any()).toBe(true);
});

整合測試重點是測試之間的關係,而測試假資料只是一種手段,若能直接測試 getTitle() 是有被呼叫到,會更符合整合測試的精神。

使用 spyOn() 回傳一個物件,在 expect() 內測試 spy.calls.any(),若該 spyOn() 的 function 有被呼叫到則為 true

toHaveBeenCalled()

1
2
3
4
5
6
it(`should use getTitle() in HTML`, () => {
spyOn(component, 'getTitle');
fixture.detectChanges();

expect(component.getTitle).toHaveBeenCalled();
});

以整合測試的角度,我們想測試的是之間的關係,也就是重點是有沒有被呼叫到,之前用假資料測試假資料只是一種手段,若 Jasmine 能直接 expect() 不就更理想?

Jasmine 提供了 expect().toHaveBeenCalled(),若 spyOn() 的 function 被呼叫執行過,就會傳回 true

toHaveBeenCalled() 是語意最佳的整合測試寫法,強烈推薦。

SpyOn() 使用時機


  1. 整合測試:用來對尚未實作的相依物件建立假物件,讓目前的整合測試紅燈,驅動出相依物件的單元測試紅燈
  2. 單元測試:用來對已經實作的相依物件建立假物件,讓目前的單元測試綠燈

1 是 ATDD 的理論基礎。

2 是 TDD 的理論基礎。

Conclusion


  • Jasmine 的 spyOn() 無法對 field 加以 spy,因此必須使用整合測試的基本技巧:使用假值並測試假值,但 method 可直接 spyOn(),建議直接搭配 toHaveBeenCalled()
  • 大部分人都是在單元測試使用 spyOn(),將相依物件加以隔離,但事實上在整合測試使用 spyOn() 配合 toHaveBeenCalled(),還可以驅動出相依物件的單元測試,這正是 ATDD 由整合測試紅燈,驅動出單元測試紅燈的關鍵技術。
2017-07-26