Stub VS. Mock
這兩個名詞時常混淆,摘錄在 Stake overflow 看到的簡潔說明 (Ref.)。
A stub is a simple fake object. It just makes sure test runs smoothly.
A mock is a smarter stub. You verify your test passes through it.
用我的話來說,就是:
- Stub:簡單的存在,讓測試可以順利執行,甚至它什麼都不做,就像一個樁 (Stub) 立在那邊。
- Mock:複雜的存在,你可以籍由它來完成測試。
一般在撰寫單元測試時,大多數都是 Stub 的應用,比如透過 jasmine 建立 Spy,搭配 returnValue()
製造出可預期的值加以驗證;或是不再加工,僅透過 expect(spy).toHaveBeenCalled()
來驗證是否有有被呼叫。
這點我們也可以在 Jasmine 的文件中看到,它是這麼解釋 .and.stub()
方法的
Tell the spy to do nothing when invoked. This is the default.
這些都很單純,呼叫方法,驗證結果;呼叫方法,驗證它有被呼叫。 (註:其實能知道有被呼叫,或呼叫了幾次,已略高級於 Stub,故有些人直接以 Spy 來稱呼它)。
那麼 Mock
是什麼?試想今天我們想要驗證重點不是結果,而是「過程」,這時候我們需要一個東西來幫我們看著、記錄、反應這個過程,它就是 Mock
,通常這會發生在回傳型別為 void
的方法上。
這邊以 Angular 當中父子 Component 的測試作為例子。
我們有 A 與 B 兩個元件 (Component),我們要驗證 A.exec()
執行時,會先呼叫 B 的 B.init()
再執行 B.exec()
,也就是要確保「執行順序」。
class AComponent {
@ViewChild(BComponent) b: BComponent;exec(): void {
b.init();
b.exec();
}
}class BComponent {
init(): void {
console.log('init');
}
exec(): void {
console.log('exec');
}
}
為此我們可以準備一個 MockBComponent
,如下:
class MockBComponent extends BComponent {
records: string[] = [];
override init(): void {
this.records.push('init');
}override exec(): void {
this.records.push('exec');
}
}
接著在測試時,將把 A.b
替換為 MockBComponent
的實例 (Instance)。
describe('AComponent', () => {let a: AComponent;
let mockB: MockBComponent;
let fixture: ComponentFixture<AComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AComponent],
schemas: [NO_ERRORS_SCHEMA],
});
fixture = TestBed.createComponent(AComponent);
a = fixture.componentInstance;
});
beforeEach(() => {
mockB = new MockBComponent();
a.b = mockB;
});
it('should...', () => {
// 由於 b 元件已經被換為 Mock,這邊 a.exec() 執行的作用將發生在 MockB 當中
a.exec();// 而 MockB 擴充了 records 屬性,我們可以從中讀取它記錄到的執行順序
// 這邊使用 .toEqual 來驗證,而非 .toBe
expect(mockB.records).toEqual(['init', 'exec']);
});});
在上述例子,我們透過 MockBComponent
賦予記錄、觀察特定行為的能力,好讓測試案例可以基於這些記錄進行驗證。
當然,我們可能有更聰明與強健的做法,例如直接讓 BComponent
擁有檢查的能力,在還沒有 init()
前,執行其他操作拋出錯誤,如此從更根源的地方決解問題,測試就不需變得複雜。