如何在 Angular 對 Service 單元測試 ?
凡與顯示相關邏輯,我們會寫在 component;凡與資料相關邏輯,我們會寫在 service。而 service 最常見的應用,就是透過 HttpClient 存取 API。
對於 service 單元測試而言,我們必須對 HttpClient 加以隔離;而對 component 單元測試而言,我們必須對 service 加以隔離,我們該如何對 service 與 component 進行單元測試呢 ?
Version
Angular CLI 1.6.2
Node.js 8.9.4
Angular 5.2.2
User Story

- Header 會顯示
Welcome to app! - 程式一開始會顯示
所有 post - 按
Add Post會呼叫POSTAPI新增 post - 按
List Posts會呼叫GETAPI 回傳所有 post
Task
- 目前有
PostService使用HttpClient存取 API,為了對PostService做單元測試,必須對HttpClient加以隔離 - 目前有
AppComponent使用PostService, 為了對AppComponent做單元測試,必須對PostService加以隔離
Architecture
AppComponent負責新增 post與顯示 post的介面顯示;而PostService負責 API 的串接- 根據
依賴反轉原則,AppComponent不應該直接相依於PostService,而是兩者相依於 interface - 根據
介面隔離原則,AppComponent只相依於它所需要的 interface,因此以AppComponent的角度訂出PostInterface,且PostService必須實作此 interface - 因為
AppComponent與PostService都相依於PostInterface,兩者都只知道PostInterface而已,而不知道彼此,因此AppComponent與PostService徹底解耦合 - 透過 DI 將實作
PostInterface的PostService注入到AppComponent,且將HttpClient注入到PostService
Implementation
AppComponent 與 PostService 的實作並非本文的重點,本文的重點在於實作 AppComponent 與 PostService 的 單元測試 部分。
PostService
post.service.spec.ts
1 | import { TestBed } from '@angular/core/testing'; |
14 行
1 | TestBed.configureTestingModule({ |
Angular 有 module 觀念,若使用到了其他 module,必須在 imports 設定;若使用到 DI,則必須在 providers 設定。
若只有一個 module,則在 AppModule 設定。
但是單元測試時,並沒有使用 AppModule 的設定,因為我們可能在測試時使用其他替代 module,也可能自己實作 fake 另外 DI。
Angular 提供了 TestBed.configureTestingModule(),讓我們另外設定跑測試時的 imports 與 providers 部分。
15 行
1 | imports: [ |
原本 HttpClient 使用的是 HttpClientModule,這會使得 HttpClient 真的透過網路去打 API,這就不符合單元測試 隔離 的要求,因此 Angular 另外提供 HttpClientTestingModule 取代 HttpClientModule。
18 行
1 | providers: [ |
由於我們要測試的就是 PostService,因此 PostService 也必須由 DI 幫我們建立。
但因爲 PostService 是基於 PostInterface 建立,因此必須透過 PostInterfaceToken mapping 到 PostService。
23 行
1 | postService = TestBed.get(PostInterfaceToken, PostService); |
由 providers 設定好 interface 與 class 的 mapping 關係後,我們必須透過 DI 建立 postService 與 mockHttpClient。
其中 HttpTestingController 相當於 mock 版的 HttpClient,因此取名為 mockHttpClient。
TestBed.get() 其實相當於
new,只是這是藉由 DI 幫我們new而已
31 行
1 | it(`should list all posts`, () => { |
先談談如何測試 GET。
3A 原則的 arrange 習慣都會寫在最前面,但在 service 的單元測試時,arrange 必須寫在最後面,否則會執行錯誤,稍後會解釋原因。
32 行
1 | /** act */ |
直接對 PostService.listPost$() 測試,由於 listPost$() 回傳 Observable,因此 expect() 必須寫在 subscribe() 內。
將預期的測試結果寫在 expected 內。
一般
Observable會在subscribe()後執行,不過在HttpTestingController的設計裡,subscribe()會在flush()才執行,稍後會看到flush(),所以此時並還沒有執行expect()測試
46 行
1 | /** arrange */ |
之前已經使用 HttpClientTestingModule 取代 HttpClient,HttpTestingController 取代 HttpClient,這只能確保呼叫 API 時不用透過網路。
還要透過 expectOne() 設定要 mock 的 URI 與 action,之所以取名為 expectOne(),就是期望有人真的呼叫這個 URI 一次,且必須為 GET,若沒有呼叫這個 URI 或者不是 GET,將造成單元測試 紅燈。
這也是為什麼 HttpTestingController 的設計是 act 與 assert 要先寫,最後再寫 arrange,因為 HttpTestingController 本身也有 assert 功能,必須有 act,才能知道 assert URI 與 GET 有沒有錯誤。
最後使用 flush() 設定 mock 的回傳值,flush 英文就是 沖水,當 HttpTestingController 將 mockResponse 沖出去後,才會執行 subscribe() 內的 expect() 測試。
也就是說若你忘了寫
flush(),其實單元測試也會綠燈,但此時的綠燈並不是真的測試通過,而是根本沒有執行到subscribe()內的expect()。
81 行
1 | afterEach(() => { |
實務上可能真的忘了寫 expectOne() 與 flush(),導致 subscribe() 內的 expect() 根本沒跑到而造成單元測試 綠燈,因此必須在每個單元測試跑完補上 mockHttpClient.verify(),若有任何 API request 卻沒有經過 expectOne() 與 flush() 測試,則 verify() 會造成單元測試 紅燈,藉以彌補忘了寫 expectOne() 與 flush() 的人為錯誤。
Q : 我們在
listPosts$()的單元測試到底測試了什麼 ?
- 若 service 的 API 與 mock 不同,會出現單元測試
紅燈,可能是 service 的 API 錯誤 - 若 service 的 action 與 mock 不同,會出現單元測試
紅燈,可能是 service 的 action 錯誤 - 若 service 的 response 與 expected 不同,可能是 service 的邏輯錯誤
61 行
1 | it(`should add post`, () => { |
再來談談如何測試 POST。
62 行
1 | /** act */ |
直接對 PostService.addPost() 測試,由於 addPost() 回傳 Observable,因此 expect() 必須寫在 subscribe() 內。
將預期的測試結果寫在 expected 內。
74 行
1 | /** arrange */ |
因為要 mock POST,因此 method 部分改為 POST,其他部分與 GET 部分完全相同。
Q : 我們在
addPost()到底測試了什麼 ?
- 若 service 的 API 與 mock 不同,會出現單元測試
紅燈,可能是 service 的 API 錯誤 - 若 service 的 action 與 mock 不同,會出現單元測試
紅燈,可能是 service 的 action 錯誤 - 若 service 的 response 與 expected 不同,可能是 service 的邏輯錯誤

使用 Wallaby.js 通過所有 service 單元測試。
AppComponent
app.component.spec.ts
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |
20 行
1 | TestBed.configureTestingModule({ |
跑 component 單元測試時,一樣沒有使用 AppModule 的設定,因為我們可能在測試時使用其他替代 module,也可能自己實作 fake 另外 DI。
因此一樣使用 TestBed.configureTestingModule(),讓我們另外設定跑測試時的 imports 與 providers 部分。
24 行
1 | imports: [ |
因為在 component 使用了 two-way binding,因此要加上 FormsModule。
Q : 為什麼要 import
HttpClientTestingModule呢 ?
AppComponent 依賴的是 PostService,看起來與 HttpClient 無關,應該不需要注入 HttpClientTestingModule。
但其實 DI 並不是這樣運作,雖然 AppComponent 只用到了 PostService,但 DI 會將 PostService 下所有的 dependency 都一起注入,所以也要 import HttpClientTestingModule。
28 行
1 | providers: [ |
由於我們要測試的就是 PostService,因此 PostService 也必須由 DI 幫我們建立。
但因爲 PostService 是基於 PostInterface 建立,因此必須透過 PostInterfaceToken mapping 到 PostService。
39 行
1 | postService = TestBed.get(PostInterfaceToken, PostService); |
由 providers 設定好 interface 與 class 的 mapping 關係後,我們必須透過 DI 建立 postService 與。
53 行
1 | it(`should list posts`, () => { |
55 行
1 | const expected$ = Observable.of([ |
透過 Observable.of() 將 Post[] 轉成 Observable<Post[]>。
63 行
1 | /** arrange */ |
由於我們要隔離 PostService,因此使用 spyOn() 對 PostService 的 listPost$() 加以 mock,並設定其假的回傳值。
65 行
1 | /** act */ |
實際測試 onListPostClick()。
67 行
1 | /** assert */ |
測試 AppComponent.post$ 是否如預期。
Q : 我們在
onListPostsClick()到底測試了什麼 ?
- 若 component 的 return 與 expected 不同,可能是 component 的邏輯錯誤
70 行
1 | it(`should add post`, () => { |
72 行
1 | const expected$ = Observable.of( |
透過 Observable.of() 將 Post 轉成 Observable<Post>。
80 行
1 | /** arrange */ |
由於我們要隔離 PostService,因此使用 spyOn() 對 PostService 的 addPost 加以 mock,並設定其假的回傳值。
82 行
1 | /** act */ |
實際測試 onAddPostClick()。
84 行
1 | /** assert */ |
由於 onAddPostClick() 回傳值為 void,且 PostService.addPost() 已經有單元測試保護,因此只要測試 PostService.addPost() 曾經被呼叫過即可。

使用 Wallaby.js 通過所有 component 單元測試。
Conclusion
- 當 component 使用了 service,若要單元測試就牽涉到 DI 與
spyOn() - Service 單元測試可透過
HttpTestingController加以隔離HttpClient - Component 單元測試可透過
spyOn()加以隔離 service
Sample Code
完整的範例可以在我的 GitHub 上找到
Reference
Angular, Testing
Angular, HttpClient
Fabian Gosebrink, Testing Angular Http Service
Chris Pawlukiewicz, Simple Observable Unit Testing in Angular 2
Jesse Palmer, Testing Angular Applications