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

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

## 總結
在進行嵌套組件測試時,主要測試以下幾點:
1. 父組件向子組件傳值時,子組件不發生異常。
2. 父組件成功地向子組件傳了值。
3. 子組件成功地向父組件傳了值。
4. 子組件向父組件傳值時,父組件不發生異常。
上述幾點我們在單元測試中分別使用以下方法來進行保障:
1. 模擬API返回數據,渲染組件,成功獲取子組件說明子組件成功渲染,父組件向子組件傳值未發生異常。
2. mock掉子組件的`@Input()`屬性,變更父組件綁定到子組件的變量,重新渲染組件,斷方間諜方法并調用。
3. 觸發子組件的數據彈射,重新渲染組件,未發生異常說明父組件接收子組件彈射的數據后未發生異常。
4. mock掉父組件對應的方法,觸發數據彈出,斷言父組件對應方法被調用。
如此以來,當前組件與子分頁組件的交互便開始有單元測試這個"護身符"來"保佑"了。日后一旦某些功能被誤殺掉,單元測試便會及時跳出來告知開發者:當前代碼已經將我的某些功能誤殺了。從而避免了一些迭代開發過程中引發的關聯性錯誤。
最后移除項目中的`fit`進行整體項目測試,未發生異常說明我們當前開發并未對歷史上的其它組件功能產生影響。
| 鏈接 | 名稱 |
| ------------------------------------------------------------ | -------------------- |
| [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) | 本節源碼 |
- 序言
- 第一章 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 發布部署
- 第九章 總結