在生產(chǎn)環(huán)境中,常常會(huì)在父子組件的鏈接上出現(xiàn)一些BUG。總結(jié)來(lái)說(shuō),這些BUG大體分為兩類。第一類父組件向子組傳值的錯(cuò)誤,第二類是子組件向父組件彈值的錯(cuò)誤。除上述錯(cuò)誤,有些時(shí)候還會(huì)出現(xiàn)方法、屬性綁定失敗的錯(cuò)誤,但這往往是由于拼寫造成的(聰明的IDE以及各種工具會(huì)替我們快速的完成這一切)。
### 父組件傳值
在父子組件在互相傳值的過(guò)程中,父組件向子組件傳入了字段不全或類型不對(duì)的數(shù)據(jù)。比如子組件有以下輸入:
```typescript
@Input()
input(a: any) : void {
console.log(a.b.c.d);
}
```
此時(shí)如果父組件如果如下調(diào)用子組件:
```html
<app-子組件 [a]="{b: {}}"></app-子組件>
```
此時(shí)由于傳入的`a`并在子組件中調(diào)用`a.b.c.d`時(shí),會(huì)產(chǎn)生一個(gè)在非object上調(diào)用`d`的異常。
### 子組件彈值
子組件向上彈值產(chǎn)生的錯(cuò)誤也大多發(fā)生在數(shù)據(jù)校驗(yàn)的層面上。比如子組件向父組件彈值為`{b: {}}`,但在父組件中卻調(diào)用了`xx.b.c.d`,則仍然會(huì)發(fā)生一個(gè)在非object上調(diào)用`d`的異常。
## 子組件測(cè)試
子組件測(cè)試更準(zhǔn)備的描述應(yīng)該為:嵌套組件測(cè)試。Angular官方文檔推薦使用組件提供樁(Stub)的方式來(lái)模擬到嵌套組件。但在實(shí)際生產(chǎn)過(guò)程中,我們發(fā)現(xiàn)這種提供樁的方法并不能夠適應(yīng)子組件的變更情況,這使單元測(cè)試失去了其“保障”的作用。在使用組件提供樁的測(cè)試方案中,當(dāng)子組件有功能變更時(shí),單元測(cè)試測(cè)試通過(guò),卻在集成測(cè)試或生產(chǎn)環(huán)境中發(fā)生了錯(cuò)誤。
> [info] 盡信書不如無(wú)書,看教程也是一樣,不要完全地相信我們。此處的思想與Angular給出的測(cè)試思想相沖突,希望日后我們能夠找到更好的貼近于Angular的官方測(cè)試方案。在學(xué)習(xí)過(guò)程中:永遠(yuǎn)不要懷疑一個(gè)人,永遠(yuǎn)不要放棄懷疑一個(gè)人。
有的同學(xué)可能對(duì)使用單元測(cè)試來(lái)完成父子組件交互測(cè)試的方式有懷疑。他認(rèn)為在開發(fā)過(guò)程中,已經(jīng)手動(dòng)的完成了父子組件的交互測(cè)試,且觀察了交互的結(jié)果,所以這種使用代碼的方式來(lái)進(jìn)行父子組件嵌套測(cè)試實(shí)際上是冗余的。
其實(shí)不然,我們單元測(cè)試的目的在于保障在日后的迭代開發(fā)中,自己開發(fā)的功能不被其它的成員或是自己誤殺掉。也是就是說(shuō):?jiǎn)卧獪y(cè)試的目的并不在于保障目前組件的功能正常,而在于保障日后該組件的功能一直正常。在實(shí)際的開發(fā)中,每個(gè)組件必然不是獨(dú)立的,一個(gè)項(xiàng)目開發(fā)完成后隨即進(jìn)行維護(hù)期,如果項(xiàng)目動(dòng)作的好,還會(huì)進(jìn)行功能的修正與更新。單元測(cè)試的作用正是:保障在日后對(duì)其它關(guān)聯(lián)模塊進(jìn)行修正更新時(shí),當(dāng)前組件的功能保持正常。
既然錯(cuò)誤往往是輸入或輸出引發(fā)的,那么我們?cè)诟缸咏M件的嵌套測(cè)試中,重點(diǎn)也應(yīng)該放在輸入與輸出上。同時(shí),由于使用組件提供樁的方案不能夠適應(yīng)子組件的變化,所以在測(cè)試過(guò)程中不應(yīng)該使用組件測(cè)試樁,而是應(yīng)該使用真實(shí)的組件。
我們?cè)跍y(cè)試文件中新建一個(gè)用于父子組件交互的方法:
```typescript
+++ b/first-app/src/app/student/student.component.spec.ts
@@ -44,4 +44,10 @@ describe('StudentComponent', () => {
// 在后臺(tái)模擬數(shù)據(jù)返回以后,然后啟動(dòng)變更檢測(cè)來(lái)更新V層,斷言table列表中的`tr`大于一行。
expect(table.querySelectorAll('tr').length).toBeGreaterThan(1);
});
+
+ fit('與分頁(yè)子組件交互測(cè)試', () => {
+ // 模擬后臺(tái)立即返回?cái)?shù)據(jù),接著使用返回的數(shù)據(jù)重新渲染組件
+ getTestScheduler().flush();
+ fixture.detectChanges();
+ });
});
```
然后使用`ng t`來(lái)啟動(dòng)單元測(cè)試。
### 獲取子組件
5.6小節(jié)我們已經(jīng)掌握了在父組件中獲取子組件的方法:使用測(cè)試夾具中`debugElement`對(duì)上的`query()`方法。
```typescript
+++ b/first-app/src/app/student/student.component.spec.ts
@@ -6,6 +6,7 @@ import {getTestScheduler} from 'jasmine-marbles';
import {MockApiTestingModule} from '../mock-api/mock-api-testing.module';
import {By} from '@angular/platform-browser';
import {PageModule} from '../clazz/page/page.module';
+import {PageComponent} from '../clazz/page/page.component';
describe('StudentComponent', () => {
let component: StudentComponent;
@@ -49,5 +50,9 @@ describe('StudentComponent', () => {
// 模擬后臺(tái)立即返回?cái)?shù)據(jù),接著使用返回的數(shù)據(jù)重新渲染組件
getTestScheduler().flush();
fixture.detectChanges();
+
+ // 獲取分頁(yè)組件
+ const pageComponent = fixture.debugElement.query(By.directive(PageComponent)).componentInstance as PageComponent
+ expect(pageComponent).toBeTruthy();
});
});
```
單元測(cè)試通過(guò),說(shuō)明成功的獲取到了子組件`pageComponent`。

獲取子組件的目的不僅僅在于支持后續(xù)子組件的輸入、輸出測(cè)試。子組件獲取成功,同時(shí)也證明了子組件在初始化過(guò)程中沒(méi)有發(fā)生異常。如果后續(xù)對(duì)`input()`的測(cè)試成功,則足以說(shuō)明:父組件在初始化時(shí)綁定了子組件的輸入`input()`,而且在調(diào)用的過(guò)程中沒(méi)有發(fā)生異常。
接下來(lái)使用代碼來(lái)保障當(dāng)前組件與分頁(yè)子組件間的交互是正常且符合預(yù)期的。
### Input()測(cè)試
當(dāng)前組件在V層中如下調(diào)用了分頁(yè)組件:
```html
<app-page [page]="pageData" (bePageChange)="onPage($event)"></app-page>
```
輸入項(xiàng)為`page`并將其賦值為`pageData`,為了保障該功能日后不被誤殺,我們需要確保當(dāng)前組件的`pageData`成功的綁定到了子組件中的`page()`方法。在單元測(cè)試中,可以將被測(cè)試調(diào)用的方法`mock`掉,然后斷言這個(gè)方法被調(diào)用:
```typescript
expect(pageComponent).toBeTruthy();
+
+ // input測(cè)試,先mock掉子組件被調(diào)用的方法
+ spyOn(pageComponent, 'page');
});
});
```
跟著上面的代碼來(lái)做的話,IDE會(huì)報(bào)一個(gè)異常:在`pageComponent`上找不到`page`方法。這是由于`PageComponent`上的`page()`方法以`set`關(guān)鍵字來(lái)聲明,該聲明方式代表`page()`被看做一個(gè)字段來(lái)處理,當(dāng)i使用`pageComponent.page = 1`時(shí),則會(huì)自動(dòng)調(diào)用`set page()`方法。
由于`set page()`并沒(méi)有被看做一個(gè)方法來(lái)對(duì)象,所以`spyOn()`方式并不適用。在`Jasmine`中應(yīng)該使用`spyOnProperty(object, propertyName, accessType)`在類似`set page()`的方法中安插間諜。
```typescript
// input測(cè)試,先mock掉子組件被調(diào)用的方法
- spyOn(pageComponent, 'page');
+ const spy = spyOnProperty(pageComponent, 'page', 'set');
});
});
```
最后繼續(xù)添加注釋:
```typescript
// input測(cè)試,先mock掉子組件被調(diào)用的方法
const spy = spyOnProperty(pageComponent, 'page', 'set');
// 然后重新為當(dāng)前組件的pageData賦值
// 重新渲染子組件,觸發(fā)set page()方法
// 斷言子組件對(duì)應(yīng)的方法被成功調(diào)用
});
```
完成功能:
```typescript
// input測(cè)試,先mock掉子組件被調(diào)用的方法
const spy = spyOnProperty(pageComponent, 'page', 'set');
// 然后重新為當(dāng)前組件的pageData賦值
const pageData = {...{}, ...component.pageData}; ①
component.pageData = pageData;
// 重新渲染子組件,觸發(fā)set page()方法
fixture.detectChanges();
// 斷言子組件對(duì)應(yīng)的方法被成功調(diào)用
expect(spy).toHaveBeenCalledWith(pageData); ②
});
```
- ① `{...{}, ...data}`可以快速完成`data`對(duì)象的`clone`從而得到一個(gè)與`data`一致的新對(duì)象。
- ② `spy`此時(shí)代表的便是分頁(yè)組件上被安插了間諜的`set page()`方法。
需要注意的是,上述代碼在初始化一個(gè)`pageData`時(shí),采用的是對(duì)象`clone`的方法,這種方法是非常有必要的,它保障了數(shù)據(jù)的一致性。
### output()輸出測(cè)試
與輸入的測(cè)試大同小異,輸出測(cè)試即子組件向父組件的彈值測(cè)試。與父組件在渲染過(guò)程中向子組件主動(dòng)傳值不同,子組件向父組件的傳值一般是被動(dòng)的。我們可以通過(guò)是否成功獲取子組件來(lái)判斷父組件向子組件傳值時(shí)是否發(fā)生異常,但卻不可以以此來(lái)判斷子組件向父組件傳值是否發(fā)生異常。
所以子組件輸出測(cè)試,應(yīng)該首先模擬一下子組件的輸出,然后重新渲染組件,從而查看子組件的數(shù)據(jù)彈出是否會(huì)引發(fā)父組件異常。
```typescript
// 斷言子組件對(duì)應(yīng)的方法被成功調(diào)用
expect(spy).toHaveBeenCalledWith(pageData);
// 觸發(fā)子組件彈出并重新進(jìn)行渲染,未發(fā)生異常說(shuō)明子組件彈值后父組件可以正確處理子組件的彈出數(shù)據(jù)
});
```
真實(shí)的數(shù)據(jù)被彈出父組件未發(fā)生異常后,便可以繼續(xù)進(jìn)行output方法的調(diào)用測(cè)試了:
```typescript
// 斷言子組件對(duì)應(yīng)的方法被成功調(diào)用
expect(spy).toHaveBeenCalledWith(pageData);
// 觸發(fā)子組件彈出并重新進(jìn)行渲染,未發(fā)生異常說(shuō)明子組件彈值后父組件可以正確處理子組件的彈出數(shù)據(jù)
// output測(cè)試,先mock掉父組件的方法
// 調(diào)用子組件的彈射器,向父組件傳值
// 斷言父組件的方法被調(diào)用
});
```
思想有了,補(bǔ)充代碼便是一件像聊天一樣的事情:
一、觸發(fā)子組件彈出
```typescript
// 觸發(fā)子組件彈出并重新進(jìn)行渲染,未發(fā)生異常說(shuō)明子組件彈值后父組件可以正確處理子組件的彈出數(shù)據(jù)
pageComponent.bePageChange.emit(2);
fixture.detectChanges();
// output測(cè)試,先mock掉父組件的方法
// 調(diào)用子組件的彈射器,向父組件傳值
// 斷言父組件的方法被調(diào)用
```
為了防止子組件的數(shù)據(jù)彈出可能會(huì)觸發(fā)mockApi的數(shù)據(jù)請(qǐng)求,我們還會(huì)習(xí)慣性的在組件渲染前加入立即返回模擬數(shù)據(jù)代碼:
```typescript
// 觸發(fā)子組件彈出并重新進(jìn)行渲染,未發(fā)生異常說(shuō)明子組件彈值后父組件可以正確處理子組件的彈出數(shù)據(jù)
pageComponent.bePageChange.emit(2);
getTestScheduler().flush();
fixture.detectChanges();
// output測(cè)試,先mock掉父組件的方法
// 調(diào)用子組件的彈射器,向父組件傳值
// 斷言父組件的方法被調(diào)用
```
二、測(cè)試彈出數(shù)據(jù)成功被組件接收
```typescript
// output測(cè)試,先mock掉父組件的方法
const onPageSpy = spyOn(component, 'onPage');
// 調(diào)用子組件的彈射器,向父組件傳值
pageComponent.bePageChange.emit(1);
// 斷言父組件的方法被調(diào)用
expect(onPageSpy).toHaveBeenCalledWith(1);
});
```
測(cè)試通過(guò):

## 總結(jié)
在進(jìn)行嵌套組件測(cè)試時(shí),主要測(cè)試以下幾點(diǎn):
1. 父組件向子組件傳值時(shí),子組件不發(fā)生異常。
2. 父組件成功地向子組件傳了值。
3. 子組件成功地向父組件傳了值。
4. 子組件向父組件傳值時(shí),父組件不發(fā)生異常。
上述幾點(diǎn)我們?cè)趩卧獪y(cè)試中分別使用以下方法來(lái)進(jìn)行保障:
1. 模擬API返回?cái)?shù)據(jù),渲染組件,成功獲取子組件說(shuō)明子組件成功渲染,父組件向子組件傳值未發(fā)生異常。
2. mock掉子組件的`@Input()`屬性,變更父組件綁定到子組件的變量,重新渲染組件,斷方間諜方法并調(diào)用。
3. 觸發(fā)子組件的數(shù)據(jù)彈射,重新渲染組件,未發(fā)生異常說(shuō)明父組件接收子組件彈射的數(shù)據(jù)后未發(fā)生異常。
4. mock掉父組件對(duì)應(yīng)的方法,觸發(fā)數(shù)據(jù)彈出,斷言父組件對(duì)應(yīng)方法被調(diào)用。
如此以來(lái),當(dāng)前組件與子分頁(yè)組件的交互便開始有單元測(cè)試這個(gè)"護(hù)身符"來(lái)"保佑"了。日后一旦某些功能被誤殺掉,單元測(cè)試便會(huì)及時(shí)跳出來(lái)告知開發(fā)者:當(dāng)前代碼已經(jīng)將我的某些功能誤殺了。從而避免了一些迭代開發(fā)過(guò)程中引發(fā)的關(guān)聯(lián)性錯(cuò)誤。
最后移除項(xiàng)目中的`fit`進(jìn)行整體項(xiàng)目測(cè)試,未發(fā)生異常說(shuō)明我們當(dāng)前開發(fā)并未對(duì)歷史上的其它組件功能產(chǎn)生影響。
| 鏈接 | 名稱 |
| ------------------------------------------------------------ | -------------------- |
| [https://jasmine.github.io/tutorials/spying_on_properties](https://jasmine.github.io/tutorials/spying_on_properties) | Spying on properties |
| [https://github.com/mengyunzhi/angular11-guild/archive/step7.4.4.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.4.4.zip) | 本節(jié)源碼 |
- 序言
- 第一章 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 注銷
- 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é)