好一陣子沒寫單元測試與整合測試了,大家是否覺得有些生疏了呢?
之前的測試都寫得很簡單,正好昨天好好地寫了搜尋輸入框還有呼叫 API ,可以藉由撰寫這個功能的測試來分享一些小技巧給大家。
小提醒:昨天的程式碼大家可以從 Github - Branch: day24 上 Clone 或者是 Fork 下來。
實作開始
這次要撰寫測試的檔案比較多,有三個 Pipe
、 一個 Service
與一個 Component
的測試需要撰寫。
不過雖然檔案比較多,但要撰寫的測試其實不會比較難,相反地,由於我們昨天在開發的時候有把邏輯切到各個 Pipe
與 Service
,因此凡而在撰寫測試上會顯得更加地好寫。
測試單元 - BooleanInZhTwPipe
首先,我們來看看最簡單的 BooleanInZhTwPipe
,其程式碼如下:
1 | export class BooleanInZhTwPipe implements PipeTransform { |
BooleanInZhTwPipe
只有一個函式 transform
,因此我們只要驗證:
- 當傳入的
value
為true
時,則回傳是
。 - 當傳入的
value
為false
時,則回傳否
。
夠簡單了吧?
測試程式碼如下:
1 | describe('BooleanInZhTwPipe', () => { |
測試結果:
測試單元 - GoogleMapLinkPipe
GoogleMapLinkPipe
的部份也很簡單,其程式碼如下:
1 | export class GoogleMapLinkPipe implements PipeTransform { |
而其驗證項目只需要驗證將傳入的第一個參數的 PositionLat
跟 PositionLong
是否有與 URL 相結合即可。
其測試程式碼如下:
1 | describe('GoogleMapLinkPipe', () => { |
測試結果:
測試單元 - LocationStringPipe
最後一個 Pipe ─ LocationStringPipe
的程式碼如下:
1 | export class LocationStringPipe implements PipeTransform { |
其驗證項目只需要驗證將傳入的第一個參數的 PositionLat
跟 PositionLong
是否有變成字串並在其中加上逗號即可。
其測試程式碼如下:
1 | describe('LocationStringPipe', () => { |
測試結果:
至此, Pipe 的部份就全測完了,相信大家這部份一定沒什麼問題。
而大家應該也有發現,我們在今天在驗 Pipe 的時候跟在驗 Component 的時候有一個滿明顯的不同,那就是我們今天沒有 TestBed
。
其實這是因為我們的這幾個 Pipe 很乾淨,沒有依賴任何其他的 Class ,所以在撰寫測試時,其實就把它當成一般的 Class ,用 new xxxPipe()
的方式產生出實體就行了。
ReactiveFormsAutoCompleteSearchingService
剛剛前面的 Pipe 只是先讓大家熱熱身,抓抓手感,接下來我們要為 ReactiveFormsAutoCompleteSearchingService
撰寫測試,算是今天的重頭戲之一。
雖然 ReactiveFormsAutoCompleteSearchingService
的程式碼也很簡單,但為什麼會是今天的重頭戲呢?
這是因為 ReactiveFormsAutoCompleteSearchingService
有用到我們之前沒有用過的 httpClient
。
先來看看它的程式碼:
1 | export class ReactiveFormsAutoCompleteSearchingService { |
ReactiveFormsAutoCompleteSearchingService
跟上面的 Pipe 一樣,都只有一個函式,不過在這個函式裡我們會需要驗兩個情境,四個案例:
- 呼叫
searchStation
所帶入的參數是空字串時- 該函式會回傳一個
Observable
(單元測試) - 要呼叫
httpClient
的get
函式,並帶入參數https://ptx.transportdata.tw/MOTC/v2/Rail/Metro/Station/TRTC?$format=JSON
(整合測試)
- 該函式會回傳一個
- 呼叫
searchStation
所帶入的參數是有效字串時- 該函式會回傳一個
Observable
(單元測試) - 要呼叫
httpClient
的get
函式,並帶入參數https://ptx.transportdata.tw/MOTC/v2/Rail/Metro/Station/TRTC?$format=JSON&$filter=contains(StationName/Zh_tw,'xxx')
(整合測試)
- 該函式會回傳一個
開始撰寫測試之前,我們一樣先把 ReactiveFormsAutoCompleteSearchingService
所依賴的項目準備好:
1 | beforeEach(() => { |
準備好依賴項目之後,就可以開始撰寫測試程式囉。
看仔細噢!原本 Service 要使用
HttpClient
的話,正常要在模組內引入HttpClientModule
。但在撰寫測試時,我們要引入的是
HttpClientTestingModule
這個 Angular 幫我們準備好專門給撰寫測試所要引入的 Module 。
我的測試程式碼如下:
1 | describe('searchStation', () => { |
測試結果:
ReactiveFormsAutoCompleteSearchingComponent
最後要測的是 ReactiveFormsAutoCompleteSearchingComponent
,由於是 Component 的關係,基本上除了 Class 本身之外,我們還要來驗證 Template 的部份。
先來看看 Class 的程式碼:
1 | export class ReactiveFormsAutoCompleteSearchingComponent { |
這個 Component 要驗的情境有:
- 驗證
searchingInputControl
是不是FormControl
- 驗證
stations$
是不是Observable
- 驗證
stations$
被訂閱時,ReactiveFormsAutoCompleteSearchingService
的函式searchStation
會不會被呼叫並傳入空字串 - 驗證
searchingInputControl
的值變動時,ReactiveFormsAutoCompleteSearchingService
的函式searchStation
會不會被呼叫並傳入searchingInputControl
的值 - 驗證
searchingInputControl
的值快速變動兩次時,ReactiveFormsAutoCompleteSearchingService
的函式searchStation
是否只被呼叫一次 - 驗證
searchingInputControl
的值變動兩次的間隔時間超過 500 毫秒時,ReactiveFormsAutoCompleteSearchingService
的函式searchStation
是否被呼叫兩次
開始測試前,一樣先把依賴的項目準備好:
1 | describe('ReactiveFormsAutoCompleteSearchingComponent', () => { |
從上述程式碼中,大家可能會發現以前從來沒看過的程式碼:
1 | { |
而這也是我們今天文章的主軸, DI 抽換 。
DI 抽換
DI ,也就是 Dependency Injection ,依賴注入。
這點大家應該知道,而 DI 抽換是 Angular 提供的一個很有趣的功能,讓我們可以用以下三種方式替換掉想替換的 Provider :
useClass
─ 提供一個繼承於想替換掉的 Provider 的 Class ,然後用新的 Class 取代原本的 Provider像是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class MyRouter extends Router {
// ...
}
@NgModule({
// ...
providers: [
{
provide: Router,
useClass: MyRouter
}
]
})
export class AbcModule { }useValue
─ 像剛剛在測試程式碼裡所寫的那樣,直接用物件抽換掉想換掉的 ProvideruseFactory
─ 用函式來抽換,像是:1
2
3
4
5
6
7
8
9
10
11
12
13
14const abcServiceFactory = () => {
return new AbcService();
}
@NgModule({
// ...
providers: [
{
provide: AbcService,
useClass: abcServiceFactory
}
]
})
export class ABCModule { }
關於這部份,真的要講很細的話可以寫一整篇,不過我今天只是想讓大家知道我們可以透過 DI 抽換的方式,把不可控的依賴變成可控的,這樣才能寫出優秀的測試。
關於 DI 抽換的部分,如果想了解更多可以參考官方的 Dependency providers 文件。
知道 DI 抽換是什麼概念之後,我們就來開始撰寫測試案例吧!
我的測試程式碼如下:
1 | describe('Property searchingInputControl', () => { |
測試結果:
在上述的測試程式碼中,我們可以看到今天要分享給大家的最後一個技巧:非同步測試。
Angular 的非同步測試技巧
在驗證非同步事件處理邏輯如 Promise
與 Observable
時,最簡單的方式當然就是直接 then
或是 subscribe
之後再驗證。
而這時我們會在傳入 it
的函式裡,多一個名為 done
的參數 (你要取名為別的名字也可以) ,如此我們就可以讓測試知道我們要等非同步事件完成後再行驗證。
像這樣:
1 | it('description', (done) => { |
但除了這個方式外,Angular 還有提供另一個方式是是永 fakeAsync
與 tick
的組合。
使用方式是將原本要傳入 it
裡的函式傳入 fakeAsync()
裡並用它來做替代,接著就可以在 it
裡面使用 tick()
這個函式來代表時間的流逝。
例如:
1 | it('description', fakeAsync(() => { |
而且這個時間的流逝是假的,又或者是說,有種「時間加速器的概念」。
假設 Do A
到 Assert A
之間相隔十年,用了 tick(10年)
之後,瞬間就過完了十年,厲害吧!
簡直媲美薩諾斯收集完無限寶石之後,一彈指就讓全宇宙的一半人口都灰飛湮滅的帥度
今天差不多就到這邊,訊息量應該滿大的,至於剩下 Template 的測試沒什麼太特別的地方,就讓大家練習做做看囉!
本日小結
今天的重點:
- 如果被測試的 Class 沒有任何依賴,則只需使用
new XXX()
來產生實體即可( Component 除外) - 如果有使用到 HttpClient 的話,撰寫測試時要引入的是
HttpClientTestingModule
,而不是HttpClientModule
- DI 抽換
- 非同步的處理
以上技巧會在大家實際撰寫測時非常大量的使用,記得要多加練習才會熟能生巧噢!
今天的程式碼會放在 Github - Branch: day25 上供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!
如果有任何的問題或是回饋,還請麻煩留言給我讓我知道!