用 Angular Component 來理解 Mock 與 Stub

Eric Li
hefemk
Published in
4 min readNov 6, 2022

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() 前,執行其他操作拋出錯誤,如此從更根源的地方決解問題,測試就不需變得複雜。

--

--