前言
在撰寫 Angular 測試時 (使用 Jasmine),我們會透過 TestBed
搭建整個被測對象。而為了讓測試順利執行,相依的資源(如各種服務),通常會在 providers 動手腳,傳入事先準備好的替代物件。
如下例,透過 useValue
傳入了事先準備好的樁(Stub)物件。而這種做法卻會引發後續 spyOn
作用對象出乎預料,本文將說明問題成因與應對方式。
const myServiceStub: Partial<MyService> = {
getValues: () => [1, 2, 3]
};await TestBed.configureTestingModule({
declarations: [ MyComponent ],
imports: [],
schemas: [NO_ERRORS_SCHEMA],
providers: [
{ provide: MyService, useValue: myServiceStub }
]
})
.compileComponents();
動態改變 Stub 回傳值
每個測試案例可能需要不同的回傳資料,目前最原始的 Stub 當中 getValues()
會回傳 [1, 2, 3]
,如果我們的測試案例需要它回傳 [4, 5, 6]
好讓實作程式走向特定路線,則可以透過 spyOn
的方式來處理。
為了節省程式碼篇幅,假設 component.getValue()
方法會呼叫 myService.getValue()
。
it('should ...', () => {
spyOn(myServiceStub, 'getValues').and.returnValue([4, 5, 6]);
expect(component.getValue()).toBe([4, 5, 6]);
});
然後,測試就會失敗了,執行時仍會拿到 [1, 2, 3]
。
先講個解決方法
歸根究柢,這個問題源自物件實體不同,意即我們 spyOn
的物件不是測試執行時所用的物件 (後面再說明)。要解決這個問題,我們只要拿到正確的物件實體再來加工即可。
我習慣透過 TestBed
先取出已注入的 Service,再來進行加工。在 Angular 9 之前可以使用 TestBed.get()
,之後使用 TestBed.inject()
。
const myService = TestBed.inject(MyService)
spyOn(myService, 'getValues').and.returnValue([4, 5, 6]);
而 StackOverflow 上的問答,也有人提供使用 useFactory 的方式注入,即可解決這個問題。
useValue 怎麼了
這是 Angular 的一個 Bug (#10788),早在 Angular 2 版本就被提出 (2016/08)。這個問題是,當使用 useValue
的方式注入時,Angular 會自行 clone 它,而不是使用同一個物件實體。
於是,讓我們重新思考一下前面寫的測試,由於 useValue
clone 了新的物件實體,因此在準備 TestBed
時我們所建立的 Stub object 與測試執行所使用的 Stub object 是兩個不同的物件實體,也就理所當然使 spyOn
看起來沒有產生作用。
useValue 修好了嗎?
在寫這篇文章 (2021/01) 時,從 #10788 的回應來看,2020/01/24 已在 Ivy 當中修正。這意謂著你在 Angular 9 之後(預估時間),要在測試時啟用 Ivy 編譯才能如預期執行。
經過實驗,我確實在 Angular 10 + Ivy 情況下,拿到正確的物件實體,測試也正常通過。
但到目前為止我還不太喜歡在測試時啟用 Ivy,當然這因人、因專案規模與架構而異,若您也不想在測試啟用 Ivy,現階段比較無害的方法還是先 TestBed.inject()
或是透過 useFactory
注入資源。