Angular DI “useValue” & Mocking

Eric Li
hefemk
Published in
4 min readJan 23, 2021
Photo by Natasya Chen on Unsplash

前言

在撰寫 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 注入資源。

--

--