釐清假資料的定義

初學者在學習測試時,常被一些測試的專有名詞迷惑,而且常常不同 framework 對同一個名詞的定義還不一樣。

本文以 Martin Fowler 在 TestDouble 一文的定義為標準,並搭配 Jasmine 解釋。

Test Double


Test Double is a generic term for any case where you replace a production object for testing purpose. There are various kinds of double list : Dummy、Fake、Stub、Mock and Spy.

白話:

Test Double 為假物件的統稱,Dummy、Fake、Stub、Mock 與 Spy 都算 Test Double。

Angular Testing 中,也是以 Test Double 統稱假物件。

Dummy


Objects are passed around but never actually used. Usually they are just used to fill parameter lists.

白話:測試時為了通過編譯而傳進假物件給 function,但這個假物件在測試中從來都沒被使用過。

1
2
3
4
5
6
7
8
it(`should have getTitle() to return 'todos'`, () => {
const dummy = {
lang: 'en-us'
};

const target = new TitleComponent();
expect(target.getTitle(dummy)).toBe('todos');
});

getTitle() 的參數要求傳入一物件,描述其顯示語系,不過目前語系並不是此次單元測試的重點,也就是邏輯根本不會跑到語系的部分。

但因為 getTitle() 的參數要求,只好建立 Dummy 假物件滿足編譯要求。

若參數允許 null,實務上也常常直接傳入 null 作為 Dummy。

Fake


Objects actually have working implementations, but usually take some shortcut which makes them not suitable for production.

白話:測試時自己重新實作 function 取代原 function。

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

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

expect(htmlElement.textContent).toBe('fake');
});

spyOn() 提供 and.callFake(),可以對既有的 function 加以重新實作,在測試時,跑的就是假 function,而非原本的 function。

Stub


Stub provide canned answers to calls made during the test.

白話:測試時建立包含假資料的假物件,並測試假資料。

1
2
3
4
5
6
7
8
9
it(`should have 'onChange()' to make 'selectedId' as selected value`, () => {
const stub = <HTMLSelectElement>{
'value': '1'
};

const target = new AppComponent();
target.onChange(stub);
expect(target.selectedId).toBe('1');
});

onChange() 要求傳入 HTMLSelectElement 物件,且測試也會用到 HTMLSelectElement 物件,此時可傳入包含假資料的 Stub 假物件,並在 expect() 測試回傳是否為 Stub 物件的假資料。

Dummy 與 Stub 的差異在於:Dummy 只是個花瓶,不會真的拿來測試,但會拿 Stub 的假資料來測試。

Mock


Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive.

白話:測試時不在乎 function 回傳值,只在乎該 function 有沒有被呼叫過。

1
2
3
4
5
6
it(`should use 'onChange()' on 'change' event`, () => {
spyOn(component, 'onChange');
debugElement.query(By.css('#TDDSelect')).triggerEventHandler('change', null);

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

在測試時使用 triggerEventHandler() 觸發了 change event,因此我們要測試的是被 Mock 的 onChange() method 是否有被執行過,因此在 expect() 使用了 toHaveBeenCalled() 測試。

Jasmine 這裏的 Spy 實質上是個 Mock,只是 Jasmine 用了 Spy 這個單字。

Spy


Spies are stubs that also record some information based on how they were called.

白話:測試時當 Stub 與 Mock 無法滿足測試時,自己寫 Spy 埋入一段測試邏輯。

實務上 Jasmine 提供的方法已經相當夠用,不太需要自己埋測試邏輯,不過當要 mock 沒有 class 的 function 或 arrow function 時,就會用到 Spy。

shipping.service.ts

1
2
3
4
5
6
7
8
import { Injectable } from '@angular/core';

@Injectable()
export class ShippingService {
calculateFee(weight: number, logistics: (number) => number): number {
return logistics(weight);
}
}

計算運費,因為依賴反轉,我們希望 user 自己提供計算運費的邏輯,而非寫死在底層。

shipping.service.spec.ts

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

target.calculateFee(null, spy);

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

Service 的單元測試,因為運算邏輯由 user 提供,因此只要測試到使用到 arrow function 即可。

1
const spy = jasmine.createSpy('logistics');

使用 jasmine.createSpy() 建立了 arrow function 的 Spy。

1
target.calculateFee(null, spy);

執行 service 的 calculateFee() method,因為不是要測試結果,因此 weight 參數並不重要,第 1 個參數傳入 null 即可,此時的 null 就是 Dummy。

第 2 個參數就是我們要測試的重點,判斷是否有被呼叫到,因此傳入 Spy。

1
expect(spy).toHaveBeenCalled();

判斷 Spy 是否有被呼叫到。

實務上會自己建立 Spy mock 沒有 class 的 function 或 arrow function。

Conclusion


  • 一般人習慣稱為 Mock,但實際上 Test Double 才是觀念上的統稱。
  • Dummy 主要是提供測試用不到的假資料,但 Stub 是測試用得到的假資料。
  • Jasmine 將 Mock 與 Spy 統稱為 Spy,不過嚴格來說,Mock 與 Spy 觀念上仍然不同。

Reference


Angular, Angular Testing
Martin Fowler, TestDouble
Martin Fowler, Mocks Aren’t Stubs
Michal Lipski, Test Doubles - Fakes, Mocks and Stubs
91, Stub, Mock, Fake 簡介

2017-08-12