本節我們補充下單元測試的代碼,來展示下當前組件的單元測試應該是什么樣子的。
## 列表初始化
在前面的單元測試中,實際上我們仍然采用了最**喜愛**的觀察手法來進行開發。單元測試僅僅起到了脫離業務邏輯獨立開發組件的作用。而我們講單元測試的作用應該是保證代碼正確執行,從而替代我們肉眼的觀察。那么,用這種**保障**的思想來寫單元測試會是個什么樣子呢?又該怎么去**想**單元測試應該怎么寫呢?
其實只要在單元測試中把我們希望用肉眼看到的結果寫出來就好。如果我們愿意,我可以把一些組件中難以用肉眼觀察到的中間態給寫出來。以當前列表初始化為列,單元測試大概應該這么寫:
- 在后臺模擬數據返回以前,斷言table列表中的`tr`僅有標題一行。
- 在后臺模擬數據返回以后,然后啟動變更檢測來更新V層,斷言table列表中的`tr`大于一行。
上述兩個斷言不正是我們用肉眼觀察后在心中判斷組件是否執行的結果嗎?帶上這個思想,我們在單元測試中先補充一些注釋:
```typescript
+++ b/first-app/src/app/student/student.component.spec.ts
@@ -31,7 +31,9 @@ describe('StudentComponent', () => {
});
fit('onInit', () => {
+ // 在后臺模擬數據返回以前,斷言table列表中的`tr`僅有標題一行。
getTestScheduler().flush();
fixture.autoDetectChanges();
+ // 在后臺模擬數據返回以后,然后啟動變更檢測來更新V層,斷言table列表中的`tr`大于一行。
});
});
```
在5.6節中我們使用了`fixture.debugElement.query(By.directive(NavComponent));`來獲取導航(菜單)組件;在6.2.3小節中我們使用了`fixture.debugElement.query(By.css('select'))`來獲取過select元素;在6.6.4小節中我們使用了`fixture.debugElement.query(By.css('nav'))`來獲取導航元素。
> `@angular/platform-browser`中的`By`除了支持`directive()`、`css()`選擇器以外,還支持:`all()`方法。
此時我們同樣可以使用`By.css()`來獲取到`table`元素,然后對`table`中的`tr`數量進行斷言,當然這需要一些`html DOM`和`css選擇器`知識。
```typescript
fit('onInit', () => {
// 在后臺模擬數據返回以前,斷言table列表中的`tr`僅有標題一行。
+ const table = fixture.debugElement.query(By.css('table')).nativeElement as HTMLTableElement;
+ console.log(table);
getTestScheduler().flush();
fixture.autoDetectChanges();
// 在后臺模擬數據返回以后,然后啟動變更檢測來更新V層,斷言table列表中的`tr`大于一行。
```
在教程中我們大量的使用在`console.log()`來打印數據, 這在開發的初期是非常有必要的。否則很難做到對每行代碼的作用、數據的類型了然于胸。此時單元測試將在控制臺打印獲取到`table`元素。

此時我們當擊該信息最左側的三角符號能夠查看此元素的具體屬性及方法,點最右側那個類似于方框的符號將自動定位到對應的`html`元素。
需要注意的是,我們在控制臺中查到的**對象**值是該對象在我們**查看**時最終值,而非我們在打印時的臨時值。我們以輸出該元素的高度為例:
```typescript
fit('onInit', () => {
// 在后臺模擬數據返回以前,斷言table列表中的`tr`僅有標題一行。
const table = fixture.debugElement.query(By.css('table')).nativeElement as HTMLTableElement;
console.log('打印的非對象類型,在控制臺查看到的是執行代碼時的即時值。當前table的高度為:', table.clientHeight);
console.log('打印對象類型,在控制臺查看到的是該對象的最終值。', table);
getTestScheduler().flush();
fixture.autoDetectChanges();
// 在后臺模擬數據返回以后,然后啟動變更檢測來更新V層,斷言table列表中的`tr`大于一行。
});
```
在控制臺中直接打印`number`類型的數據,打印的為執行`console.log()`時的即時值,此時`table`中由于僅僅有一行標題,所以高度為51。

在控制臺中打印類型為`HTMLTableElement`的對象,則在控制臺中查看到的是該對象的最終值。在最終狀態`table`已經填充了模擬后臺返回數據,此時不但有一個標題,還有20行數據,所以高度為1170。

這是由于`console.log()`接收到對象以后,實際上是記錄了該對象在**引用**值,在C語言中把這個**引用**稱為指針。
## 指針
基本上所有的語言都使用了C語言中**指針**的思想,如果它們不這么做,那么在進行函數的調用時則需要占用不可控的內存或是面臨如何處理對象間循環套用的問題。比如JAVA中的堆和棧,再比如javascript的引用傳值,或是PHP的對象傳遞等實際上都是進行指針傳遞。
`console.log()`方法同樣也是如此,由于接收的對象的復雜性未知,當接著的參數類型為對象時,無論是站在時間的角度上,還是空間的角度上,它都很難存儲這個對象的快照。而存儲該對象的引用則是最好的方法。在用戶在控制中查看數據時,再去內存中獲取相應的值進而顯示在控制臺中。
這就是為什么直接打印的talbe高度會在控制臺中顯示即時值,而打印table則會顯示最終值的原因。
既然`console.log()`在處理對象時查看是最終值,那么我們在開發過程中,又如何在控制臺中查看某個對象的即時值呢?
## debug
要想查看對象的即時值,則需要借助瀏覽器的debug功能。下面,我們分別就`firefox`及`chrome`瀏覽器做debug展示。
### firefox
在前面我章節中,我們學習使用了控制臺中的Inspetor、Console、Network以及Applicaton。查看在執行過程中對象的瞬時值,則需要使用Debugger:

點擊Debuger后,點擊 `Go to file 打開文件`,在彈出的對畫框中輸入我們當前的測試文件:

邊輸入`firefox`會邊把符合要求的文件過濾出來,此時我們選擇正在測試的文件后將在Debugger中查看到該文件,然后找到變量table的位置,并在該變量所在行的行號上點擊一下:

點擊后該行將被點亮,此時刷新瀏覽器,單元測試執行到該行時將被暫時中斷,此時將鼠標移動到`table`變量上,則可以查看該變量的即時值:

如果我們想在控制中查看這個即時值,則可以點擊步進小圖標,使用代碼由38行執行到39行:

此時39行代碼點亮,表示程序即將執行此行代碼,也意味著38行代碼已成功執行:

然后我們來到控制臺,此時查看到的對象即為當前狀態下的即時值。

其實在debugger模式下`console.log()`并未做任何的改變,它依然是顯示了此時對象的最終值。只不過由于中斷的作用,當前對象的最終值即為當前狀態的即時值罷了。
查看完即時值后,再次點擊38行的行號,點亮效果消失,重新刷新瀏覽器恢復為正常執行。

### chrome
Chrome瀏覽器debug的方法大同小異,打開控制臺并打開Sources選項卡。此處將提示的打開特定文件所需要的快捷鍵。

比如當前為macos系統,按`command + p`后打開對話框,然后輸入預查看變量所在的文件:

該對話框同樣支持過濾功能,只需要輸入特定的關鍵字即可快速的定位到相關的文件,打開變量的所在行并點擊行號,則會設置一個斷點,刷新瀏覽器程序執行到此將被中斷。

此后的操作與firefox基本相同。把鼠標移到變量上來查看變量的即時值:

點擊步進時向下執行一行:

在控制臺中查看對象的即時值:

### 區別
`firefox`與`chrome`在對`console.log()`的處理上大同小異,但在處理細節上仍有不同。比如在打印`html DOM`時,firefox打印的是對象的屬性及方法,而chrome則打印是該對象對應的html元素代碼。至于哪個更好,則完全由你來判斷,你喜歡哪個,哪個就是最好的。
## HtmlTableElement
剛剛在調用`query(By.css('table')).nativeElement`時,將返回值看做了`HTMLTableElement`,這是由于我們確信查詢到的`table`元素的對象類型就是這個`HTMLTableElement`。當對象指定為特定的類型有個最大的好處就是可以在后續的代碼中在編輯器的幫助下快速的獲取到將對象上的屬性,或是調用該對象上的方法;最大的壞處是如果我們不小心把返回值類型`as`錯了,則可以在后續的代碼發生一系列BUG。盡管有指定錯誤的風險,在開發中我們仍然愿意使用`as`關鍵字來指定一個特定的類型。
比如我們把`table`元素準確的指定為了``HTMLTableElement`,則可以查閱`HTMLTableElement`的官方文檔,快速的獲取到該元素上的屬性、方法。
所有的`html DOM`都可以在[mozilla的官方站點](https://developer.mozilla.org/en-US/docs/Web/API)上找到,如果你還沒有完全地切換到看英文資料的習慣上,還可以查問對應的[中文官方站點](https://developer.mozilla.org/zh-CN/docs/Web/API)。我們可以在其主頁上找到`HtmlTableElement`的身影:

或是通過首頁上方的查詢框來查詢來相應的元素:

點擊后對應的鏈接后將來到 `HtmlTableElement`的首頁,首頁最上方法展示了該接口(在mozilla上統一把它們稱為接口,這是由于它把不同的瀏覽器看到了接口的具體實體)的關系圖:

上圖展示了`HtmlTableElement`接口的繼承關系,所以如果有些屬性和方法并不是`Table`元素特有的話,則可以在其父接口、父父接口中去查找。最終我們由[ParentNode](https://developer.mozilla.org/zh-CN/docs/Web/API/ParentNode)中找到的[querySelectorAll()](https://developer.mozilla.org/zh-CN/docs/Web/API/ParentNode/querySelectorAll)方法用于查詢`table`元素中的子`tr`元素:
```typescript
fit('onInit', () => {
// 在后臺模擬數據返回以前,斷言table列表中的`tr`僅有標題一行。
const table = fixture.debugElement.query(By.css('table')).nativeElement as HTMLTableElement;
console.log('打印的非對象類型,在控制臺查看到的是執行代碼時的即時值。當前table的高度為:', table.clientHeight);
console.log('打印對象類型,在控制臺查看到的是該對象的最終值。', table);
+ expect(table.querySelectorAll('tr').length).toBe(1);
getTestScheduler().flush();
fixture.autoDetectChanges();
// 在后臺模擬數據返回以后,然后啟動變更檢測來更新V層,斷言table列表中的`tr`大于一行。
});
```
在斷言相等時,我們有`toBe()`及`toEqual()`可用。兩者在多數情況下通用,但`toBe()`校驗的更為嚴格,而`toEqual()`則相對不太嚴格。
最后加入數據返回后斷言的代碼:
```typescript
fit('onInit', () => {
// 在后臺模擬數據返回以前,斷言table列表中的`tr`僅有標題一行。
const table = fixture.debugElement.query(By.css('table')).nativeElement as HTMLTableElement;
console.log('打印的非對象類型,在控制臺查看到的是執行代碼時的即時值。當前table的高度為:', table.clientHeight);
console.log('打印對象類型,在控制臺查看到的是該對象的最終值。', table);
expect(table.querySelectorAll('tr').length).toEqual(1);
getTestScheduler().flush();
fixture.autoDetectChanges();
// 在后臺模擬數據返回以后,然后啟動變更檢測來更新V層,斷言table列表中的`tr`大于一行。
expect(table.querySelectorAll('tr').length).toBeGreaterThan(1);
});
```
此時,一個替待了肉眼觀察的單元測試代碼便真正完成了。
## 度
古語說過猶不及,水滿則溢,月滿則虧。都是對**度**的一種描述。在單元測試中,很難把握一個**度**字。以當前測試為例,在開發過程中我們肉眼判斷的除了在請求數據返回后行數增加了以外,其實還對表格樣式,數據表中的每個單元格的填充文字等。如果把這些判斷都寫到單元測試中無疑可以提升系統的健壯性,但其實這樣做往往得不償失。
在實際的項目中又該如何把握這個度呢,個人認為適用就好,在適用的前提下盡量地提升單元測試代碼的測試覆蓋率。如果某段測試代碼在項目期間都沒有起過**保障**的作用,那么這些測試代碼便可以認為是無效的,在后續的開發中再遇到類似情景時則可以考慮省略掉;如果在使用過程中,發現有很多BUG點,則需要考慮應該如何增加單元測試的代碼來規避這些BUG,使用單元測試來保證此類BUG不再發生。當有一天我們使用最少的單元測試代碼,將整個項目的BUG發生率控制在一個有效地比較小的范圍內時,便找到了這個適用的點。
有些時候我們還必須考慮當前技術服務的對象,同樣的項目有1萬的資金支持與有10萬的資金支持,對單元測試的度的把控是不同的;同樣的項目有1個月的工期限制還是有3個月的工期限制,對度的把控也不應該相同。技術是為業務服務的,不存在沒有業務的技術。無論自己身處什么位置,都應該謹記:不能為了技術而技術!
| 名稱 | 鏈接 |
| -------- | ------------------------------------------------------------ |
| 本節源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step7.4.2.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.4.2.zip) |
- 序言
- 第一章 Hello World
- 1.1 環境安裝
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教師管理
- 2.1 教師列表
- 2.1.1 初始化原型
- 2.1.2 組件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 請求后臺數據
- 2.2.1 HttpClient
- 2.2.2 請求數據
- 2.2.3 模塊與依賴注入
- 2.2.4 異步與回調函數
- 2.2.5 集成測試
- 2.2.6 本章小節
- 2.3 新增教師
- 2.3.1 組件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 對接后臺
- 2.3.4 路由
- 2.4 編輯教師
- 2.4.1 組件初始化
- 2.4.2 獲取路由參數
- 2.4.3 插值與模板表達式
- 2.4.4 初識泛型
- 2.4.5 更新教師
- 2.4.6 測試中的路由
- 2.5 刪除教師
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome圖標庫
- 2.6.3 firefox
- 2.7 總結
- 第三章 用戶登錄
- 3.1 初識單元測試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類
- 3.7 瀏覽器緩存
- 3.8 總結
- 第四章 個人中心
- 4.1 原型
- 4.2 管道
- 4.3 對接后臺
- 4.4 x-auth-token認證
- 4.5 攔截器
- 4.6 小結
- 第五章 系統菜單
- 5.1 延遲及測試
- 5.2 手動創建組件
- 5.3 隱藏測試信息
- 5.4 規劃路由
- 5.5 定義菜單
- 5.6 注銷
- 5.7 小結
- 第六章 班級管理
- 6.1 新增班級
- 6.1.1 組件初始化
- 6.1.2 MockApi 新建班級
- 6.1.3 ApiInterceptor
- 6.1.4 數據驗證
- 6.1.5 教師選擇列表
- 6.1.6 MockApi 教師列表
- 6.1.7 代碼重構
- 6.1.8 小結
- 6.2 教師列表組件
- 6.2.1 初始化
- 6.2.2 響應式表單
- 6.2.3 getTestScheduler()
- 6.2.4 應用組件
- 6.2.5 小結
- 6.3 班級列表
- 6.3.1 原型設計
- 6.3.2 初始化分頁
- 6.3.3 MockApi
- 6.3.4 靜態分頁
- 6.3.5 動態分頁
- 6.3.6 @Input()
- 6.4 編輯班級
- 6.4.1 測試模塊
- 6.4.2 響應式表單驗證
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定義FormControl
- 6.4.6 代碼重構
- 6.4.7 小結
- 6.5 刪除班級
- 6.6 集成測試
- 6.6.1 惰性加載
- 6.6.2 API攔截器
- 6.6.3 路由與跳轉
- 6.6.4 ngStyle
- 6.7 初識Service
- 6.7.1 catchError
- 6.7.2 單例服務
- 6.7.3 單元測試
- 6.8 小結
- 第七章 學生管理
- 7.1 班級列表組件
- 7.2 新增學生
- 7.2.1 exports
- 7.2.2 自定義驗證器
- 7.2.3 異步驗證器
- 7.2.4 再識DI
- 7.2.5 屬性型指令
- 7.2.6 完成功能
- 7.2.7 小結
- 7.3 單元測試進階
- 7.4 學生列表
- 7.4.1 JSON對象與對象
- 7.4.2 單元測試
- 7.4.3 分頁模塊
- 7.4.4 子組件測試
- 7.4.5 重構分頁
- 7.5 刪除學生
- 7.5.1 第三方dialog
- 7.5.2 批量刪除
- 7.5.3 面向對象
- 7.6 集成測試
- 7.7 編輯學生
- 7.7.1 初始化
- 7.7.2 自定義provider
- 7.7.3 更新學生
- 7.7.4 集成測試
- 7.7.5 可訂閱的路由參數
- 7.7.6 小結
- 7.8 總結
- 第八章 其它
- 8.1 打包構建
- 8.2 發布部署
- 第九章 總結