Jasmine 測試當中建立假物件的小技巧

Eric Li
hefemk
Published in
6 min readNov 3, 2022

Angular 預設使用 Jasmine 測試框架,撰寫測試時應常接觸 Spy 系列方法,本文提供一些小技巧,讓您撰寫與維護測試時更為方便。

如何做出一個假物件

這裡以假物件來代稱,你可能在他處看到 Test doubble (測試替身) 的字眼,或聽到較口語的 Mock 說法,這邊就泛指這些東西。(註:Mock 與 Stub 有些區別,未來再寫文章說明)

那麼,為什麼要建立假物件呢?這其實是要把假物件「替換」真物件,來實現測試的獨立性,而之所以能夠替換物件,除了最單純的公開(Public)成員替換外,依賴注入(Dependency Injection, DI) 更是功不可沒 (嗯,就單純點一下,沒有要現在解釋的意思)。

回到主題,這邊列舉建立假物件時,3 個容易想到的方式:

  1. Object Literal 搭配 PartialspyOn()
  2. 使用 jasmine.createSpyObj()jasmine.createSpy()
  3. 繼承原 Class 再 Override,或基於 Interface 實作出新的 Class。

由於 3. 就是 OO 常規操作,本次就不再討論,以下針對 1. 與 2. 說明。

Object Literal + Partial + spyOn()

Object Literal 在 JavaScript 當中很直觀,直接以 {} 表達一個物件。在 TypeScript 自然也支援這種寫法,但必須注意可維護性,避免實作更改後,用在測試案例中的物件仍渾然不知。

例如:

// 原本定義的 Example 類別包含兩個方法,sayHi() 與 say()
class Example {
sayHi(): string {
return 'Hi';
}
say(value: string): string {
return value;
}
}
// Object Literal 可以隨心所欲只寫一部分,因為它實與 Example 沒有關聯
// 故這樣寫反而不利維護,當 Example 發生變化時,這裡的 Object Literal 渾然不知
const exampleStub = {
sayHi: () => 'Hi',
};

搭配 Partial 可讓編輯器自動檢查與提示,並允許我們僅定義需要的「部分」,省去完整抄寫的時間。例如:

const exampleStub: Partial<Example> = {
sayHi: () => 'Hi',
sayHello: () => 'Hello', // 編輯器會報錯,sayHello() 並不存在於 Example 當中
};

但是,這樣的假物件通常不足以滿足測試,通常還會再搭配 `spyOn()` 方法來提升它的靈活度與可測性,例如:

const exampleStub: Partial<Example> = {
sayHi: () => 'Hi';
};
describe('Example', () => {
it('should ... ', () => {
const spy = spyOn(exampleStub, 'sayHi');
exampleStub.sayHi();
expect(spy).toHaveBeenCalled();
});
});

jasmine.createSpyObj()

使用此方法,可以快速建立一個假物件,並且讓指定的方法都變成 Spy。使用時,建議傳入泛型(Generic),來強化開發工具的自動檢查與提示。

例如:

class Example {
sayHi(): string {
return 'Hi';
}
}
describe('Example', () => {
it('should ... ', () => {
// 第二參數是個 Array,傳入需要變成 Spy 的方法名稱
// 由於指定泛型,若指定的方法名稱不存在於 Example,將會立即報錯
const spy = jasmine.createSpyObj<Example>('Example', ['sayHi']);

// 若您沒有指定泛型,則誤寫為 sayHello 時也不會檢查到異常
const spy2 = jasmine.createSpyObj('Example', ['sayHello']);
});
});

透過 createSpyObj() 建立出來的物件型別是 jasmine.spyObj<T>,而其被指定的方法型別則是 jasmine.Spy<jasmine.Func>,你可以對它做一些常規操作,例如:
1. 透過 .and 串接 .returnValue().returnValues().callFake() 等。
2. 透過 expect(spy).toHaveBeenCalled() 來驗證它是否被呼叫。

再看個例子會更清楚:

// 透過 createSpyObj 方法建立 spy object
const spyObj = jasmine.createSpyObj<Example>('Example', ['sayHi']);
// 其內的 .sayHi() 方法其實是個 jasmine.Spy
const spy = spyObj.sayHi as jasmine.Spy<jasmine.Func>;
// 因此可以改變它的回傳結果
spy.and.returnValue('Hello');
// 執行以取得結果
const result = spyObj.sayHi();
// 驗證結果,確實是 'Hello'
expect(result).toEqual('Hello');
// 你也可以驗證 sayHi() 是有被呼叫的
expect(spy).toHaveBeenCalled();

jasmine.createSpy()

前面看過 createSpyObj(),那 createSpy() 又是什麼呢?它負責的範疇僅在一個方法,產生出的物件型別為 jasmine.Spy<jasmine.Func>,與前面章節介紹相同。

例如:

class Example {
sayHi(): string {
return 'Hi';
}
}
describe('Example', () => {
it('should ... ', () => {
const example = new Example();

// 此時原本的 sayHi 方法被替換為 Spy
example.sayHi = jasmine.createSpy();

// 這個 Spy 依然可以被執行
example.sayHi();

// 驗證它是否被執行
expect(example.sayHi).toHaveBeenCalled();
});
});

小結

怎麼做比較好?個人看法是都好,但請確保編輯器可以給予足夠的檢查與提示 (方法都在文內囉)。

--

--