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

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

在控制臺(tái)中打印類型為`HTMLTableElement`的對(duì)象,則在控制臺(tái)中查看到的是該對(duì)象的最終值。在最終狀態(tài)`table`已經(jīng)填充了模擬后臺(tái)返回?cái)?shù)據(jù),此時(shí)不但有一個(gè)標(biāo)題,還有20行數(shù)據(jù),所以高度為1170。

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

點(diǎn)擊Debuger后,點(diǎn)擊 `Go to file 打開(kāi)文件`,在彈出的對(duì)畫(huà)框中輸入我們當(dāng)前的測(cè)試文件:

邊輸入`firefox`會(huì)邊把符合要求的文件過(guò)濾出來(lái),此時(shí)我們選擇正在測(cè)試的文件后將在Debugger中查看到該文件,然后找到變量table的位置,并在該變量所在行的行號(hào)上點(diǎn)擊一下:

點(diǎn)擊后該行將被點(diǎn)亮,此時(shí)刷新瀏覽器,單元測(cè)試執(zhí)行到該行時(shí)將被暫時(shí)中斷,此時(shí)將鼠標(biāo)移動(dòng)到`table`變量上,則可以查看該變量的即時(shí)值:

如果我們想在控制中查看這個(gè)即時(shí)值,則可以點(diǎn)擊步進(jìn)小圖標(biāo),使用代碼由38行執(zhí)行到39行:

此時(shí)39行代碼點(diǎn)亮,表示程序即將執(zhí)行此行代碼,也意味著38行代碼已成功執(zhí)行:

然后我們來(lái)到控制臺(tái),此時(shí)查看到的對(duì)象即為當(dāng)前狀態(tài)下的即時(shí)值。

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

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

比如當(dāng)前為macos系統(tǒng),按`command + p`后打開(kāi)對(duì)話框,然后輸入預(yù)查看變量所在的文件:

該對(duì)話框同樣支持過(guò)濾功能,只需要輸入特定的關(guān)鍵字即可快速的定位到相關(guān)的文件,打開(kāi)變量的所在行并點(diǎn)擊行號(hào),則會(huì)設(shè)置一個(gè)斷點(diǎn),刷新瀏覽器程序執(zhí)行到此將被中斷。

此后的操作與firefox基本相同。把鼠標(biāo)移到變量上來(lái)查看變量的即時(shí)值:

點(diǎn)擊步進(jìn)時(shí)向下執(zhí)行一行:

在控制臺(tái)中查看對(duì)象的即時(shí)值:

### 區(qū)別
`firefox`與`chrome`在對(duì)`console.log()`的處理上大同小異,但在處理細(xì)節(jié)上仍有不同。比如在打印`html DOM`時(shí),firefox打印的是對(duì)象的屬性及方法,而chrome則打印是該對(duì)象對(duì)應(yīng)的html元素代碼。至于哪個(gè)更好,則完全由你來(lái)判斷,你喜歡哪個(gè),哪個(gè)就是最好的。
## HtmlTableElement
剛剛在調(diào)用`query(By.css('table')).nativeElement`時(shí),將返回值看做了`HTMLTableElement`,這是由于我們確信查詢到的`table`元素的對(duì)象類型就是這個(gè)`HTMLTableElement`。當(dāng)對(duì)象指定為特定的類型有個(gè)最大的好處就是可以在后續(xù)的代碼中在編輯器的幫助下快速的獲取到將對(duì)象上的屬性,或是調(diào)用該對(duì)象上的方法;最大的壞處是如果我們不小心把返回值類型`as`錯(cuò)了,則可以在后續(xù)的代碼發(fā)生一系列BUG。盡管有指定錯(cuò)誤的風(fēng)險(xiǎn),在開(kāi)發(fā)中我們?nèi)匀辉敢馐褂胉as`關(guān)鍵字來(lái)指定一個(gè)特定的類型。
比如我們把`table`元素準(zhǔn)確的指定為了``HTMLTableElement`,則可以查閱`HTMLTableElement`的官方文檔,快速的獲取到該元素上的屬性、方法。
所有的`html DOM`都可以在[mozilla的官方站點(diǎn)](https://developer.mozilla.org/en-US/docs/Web/API)上找到,如果你還沒(méi)有完全地切換到看英文資料的習(xí)慣上,還可以查問(wèn)對(duì)應(yīng)的[中文官方站點(diǎn)](https://developer.mozilla.org/zh-CN/docs/Web/API)。我們可以在其主頁(yè)上找到`HtmlTableElement`的身影:

或是通過(guò)首頁(yè)上方的查詢框來(lái)查詢來(lái)相應(yīng)的元素:

點(diǎn)擊后對(duì)應(yīng)的鏈接后將來(lái)到 `HtmlTableElement`的首頁(yè),首頁(yè)最上方法展示了該接口(在mozilla上統(tǒng)一把它們稱為接口,這是由于它把不同的瀏覽器看到了接口的具體實(shí)體)的關(guān)系圖:

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