整理最常用的 Jasmine Spy

Jasmine 是個 mock 與 assertion 合一的 testing framework,語法優美,寫起來程式碼的可讀性很高,重點是不複雜,學習曲線平緩,Spy 為 Jasmine 最重要的部分,本文針對最常在 Angular 使用的 Spy 做整理。

Version


Jasmine 2.6.2
Angular 4.3

建立 Spy


spyOn()

spyOn(object: T, method: keyof T): Spy

對於 class 的 method 建立 Spy。

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

expect(spy).toHaveBeenCalled();
});

測試在 HTML getTitle() 是否 binding 正確。

spyOnProperty()

spyOnProperty(object:T, property: keyof T, accessType: string): Spy

對於 class 的 property 建立 Spy。

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

expect(spy).toHaveBeenCalled();
});

測試在 HTML title 是否 binding 正確。

jasmine.createSpy()

createSpy(name: string, originalFn?: Function): Spy

對於 function 或 arrow function 建立 Spy。

1
2
3
4
5
6
it(`should call arrow function`, () => {
const spy = jasmine.createSpy('logistics');
target.calculateFee(null, spy);

expect(spy).toHaveBeenCalled();
});

測試 function 或 arrow function 是否被呼叫。

若要建立在 class 內的假 method,要用 spyOn();若要建立沒有 class 的假 function 或 arrow function,則要使用 jasmine.createSpy()

Spy#and


returnValue()

returnValue(val: any): Spy

建立假 function 的假回傳值。

1
2
3
4
5
6
7
it(`should use getTitle()`, () => {
const spy = spyOn(component, 'getTitle').and.returnValue('My Todos');
fixture.detectChanges();

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

使用 spyOn() 搭配 returnValue() 回傳假值,然後測試結果是否為假值。實務上有 2 個場景會使用 returnValue()

  1. 單元測試時,想要隔離相依物件,使用 returnValue() 建立假回傳值。
  2. 整合測試時,想要測試關係,不是測試結果,藉由假資料測試 function 是否被呼叫到。
1
2
3
4
5
6
it(`should use getTitle()`, () => {
const spy = spyOn(component, 'getTitle');
fixture.detectChanges();

expect(spy).toHaveBeenCalled();
});

若只是測試關係,那有更簡單的寫法,使用 spyOn() 建立 Spy 之後,測試 Spy 的 toHaveBeenCalled() 即可,不需使用 returnValue()

callFake()

callFake(fn: Function): Spy

建立假 function 的假實作。

1
2
3
4
5
6
7
it(`should use getTitle() and result is 'my todos'`, () => {
const spy = spyOn(component, 'getTitle').and.callFake(() => 'My Todos');
fixture.detectChanges();

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

一般來說,spyOn() 搭配 returnValue() 已經非常夠用,若你想自行建立 Fake 取代原本 function 實作,則可使用 callFake()

callThrough()

callThrough(): Spy

依然使用原本 function,但會針對此 function 做 Spy 監測。

1
2
3
4
5
6
7
8
9
it(`should use getTitle() and result is 'todos'`, () => {
const spy = spyOn(component, 'getTitle').and.callThrough();
fixture.detectChanges();

htmlElement = debugElement.query(By.css('h1')).nativeElement;

expect(htmlElement.textContent).toBe('todos');
expect(spy).toHaveBeenCalled();
});

一般我們使用了 spyOn() 之後,就會搭配 toHaveBeenCalled() 測試有沒有被呼叫到即可,若你想除了驗證關係外,還順便結果,則可搭配 callThrough(),此時會執行實際的 function,所以可使用 toBe() 測試結果,但仍然保有 Spy 功能,因此也可使用 toHaveBeenCalled() 測試關係。

throwError()

throwError(msg: string): Spy

建立假 function 的假 error。

1
2
3
4
5
it(`should throw error on 'getTodos()' method`, () => {
spyOn(target, 'getTodos').and.throwError('My Error');

expect(() => { target.getTodos() }).toThrowError('My Error');
});

使用 throwError() 建立假 error。

比較特別的是,要測試 error 時,待測 method 要放在 expect() 的 arrow function 內,最後用 toThrowError() 測試 error 是否被觸發。

Spy#calls


any()

any(): boolean

若 Spy 被呼叫一次以上,則傳回 true,否則傳回 false

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

expect(spy.calls.any()).toBeTruthy();
});

測試 spy.calls.any(),若曾被呼叫過為 true,否則為 false

這種寫法等效於:

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

expect(spy).toHaveBeenCalled();
});

count()

count(): boolean

傳回 Spy 被呼叫的次數。

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

expect(spy.calls.count()).toBe(1);
});

測試 spy.calls.count(),可精確測試 Spy 被呼叫的次數。

這種寫法等效於:

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

expect(spy).toHaveBeenCalledTimes(1);
});

Matcher


toHaveBeenCalled()

toHaveBeenCalled(): boolean

測試 function 是否被呼叫過。

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

expect(spy).toHaveBeenCalled();
});

測試 Spy 是否被呼叫過。

toHaveBeenCalledTimes()

toHaveBeenCalledTimes(expected: number): boolean

測試 function 是否被呼叫過 n 次。

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

expect(spy).toHaveBeenCalledTimes(1);
});

測試 Spy 是否被呼叫過 1 次。

Conclusion


  • Jasmine 的 Spy 算是 Mock 與 Spy 的合體,提供單元測試與整合測試夠用的武器,在單元測試可以藉由 Spy 隔離相依物件,在整合測試可以藉由 Spy 驗證物件之間的關係。

Reference


KeenWon, Javascript测试框架Jasmine (五):Spies
Jasmine, Jasmine Introduction

2017-08-13