本節讓我們看看如何使用代碼的方式來完成點擊教師列表的動作,初步的接觸下自動化的魅力。
當前測試用例下,我們需要模擬用戶點擊教師列表,以測試教師列表被點擊后,是否在組件的`@Output`上接收到了相應的值。比如我們模擬點擊教師列表中的最后一個教師:
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
@@ -41,5 +41,10 @@ describe('KlassSelectComponent', () => {
fixture.autoDetectChanges();
component.beChange
.subscribe((data: number) => console.log('接收到了彈出的數據', data));
+ // 模擬點擊教師列表中的第二個教師
});
});
```
### 模擬點擊option
在響應式表中,模擬點擊某個`option`僅需要以下兩步:
1. 獲取預點擊`option`對應的值
2. 將值使用`setValue()`方法送入`FormControl`
```typescript
@@ -44,6 +42,7 @@ describe('KlassSelectComponent', () => {
component.beChange
.subscribe((data: number) => console.log('接收到了彈出的數據', data));
// 模擬點擊教師列表中的第二個教師
+ const teacher = component.teachers[1];
+ console.log(teacher);
});
});
```
打開控制臺我們得到了一個undefined:

產生該問題的原因從根上來講是由于JavaScript的異步機制。從當前應用層面上來講,是由于我們的MockApi模擬了真實網絡的延遲。當前對組件的測試大概發生了以下事件:

如上圖示,由于`MockApi`的延遲發送數據,所以在執行到`const teacher = component.teachers[1];`時,`component.teachers`仍然是個空數組。
```typescript
// 模擬點擊教師列表中的第二個教師
+ console.log(component.teachers.length);
const teacher = component.teachers[1];
console.log(teacher);
```

但當前我們希望的是單元測試按以下流程執行:

## getTestScheduler()
預使在發生http請求時`MockApi`馬上發送數據,則需要使用`jasmine-marbles`提供的`getTestScheduler()`,該方法的作用是:馬上發送(本計劃延遲發送的)數據 。由于`jasmine-marbles`只被應用在測試環境下,所以我們使用`npm install --save-dev jasmine-marbles`來安裝:
```bash
panjie@panjies-Mac-Pro first-app % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app
panjie@panjies-Mac-Pro first-app % npm install --save-dev jasmine-marbles
...
+ jasmine-marbles@0.8.1
added 1 package, removed 1 package and audited 1472 packages in 9.079s
...
```
<hr>
**以下內容了解即可**
實際上`MockApi`提供了兩種延遲發送數據的方案:`MockApiInterceptor`與`MockApiTestingInterceptor`。`MockApiInterceptor`用于沒有后臺的演示環境,該延遲是通過RxJS的`delay`方法實現的;而`MockApiTestingInterceptor`專門用于測試環境,該延遲是通的RxJS的彈珠來實現的。而彈珠測試的數據發送,是可以通過`jasmine-marbles`手動控制的。
**以下內容了解即可**
<hr>
然后我們在當前動態測試模塊下引入支持`jasmine-marbles`馬上發送數據的`MockApiTestingInterceptor`來替換原`MockApiInterceptor`:
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
-import {MockApiInterceptor} from '@yunzhi/ng-mock-api';
+import {MockApiTestingInterceptor} from '@yunzhi/ng-mock-api/testing';
@@ -22,7 +22,7 @@ describe('KlassSelectComponent', () => {
providers: [
{
provide: HTTP_INTERCEPTORS, multi: true,
- useClass: MockApiInterceptor.forRoot([
+ useClass: MockApiTestingInterceptor.forRoot([
TeacherMockApi
])
}
```
接下來便可以在測試代碼中調用`getTestScheduler()`方法來手動控制數據發送了:
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
@@ -5,6 +5,7 @@ import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {TeacherMockApi} from '../../mock-api/teacher.mock.api';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MockApiTestingInterceptor} from '@yunzhi/ng-mock-api/testing';
+import {getTestScheduler} from 'jasmine-marbles';
describe('KlassSelectComponent', () => {
let component: KlassSelectComponent;
@@ -41,6 +42,10 @@ describe('KlassSelectComponent', () => {
fixture.autoDetectChanges();
component.beChange
.subscribe((data: number) => console.log('接收到了彈出的數據', data));
+
+ // 手動控制MockApi發送數據
+ getTestScheduler().flush();
+
// 模擬點擊教師列表中的第二個教師
console.log(component.teachers.length);
```

需要**注意**的是:
1. 這種數據發送方式產生的數據變化并不會被`fixture`感知到,所以如果我們需要查看V層的效果,則需要在數據發送完成后,手動調用`detectChanges()`以使V層重新進行渲染。
2. 引入`MockApiTestingInterceptor`后,在測試中僅當調用`getTestScheduler().flush();`時才會發送數據,否則不會發送數據。
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
@@ -45,6 +45,7 @@ describe('KlassSelectComponent', () => {
// 手動控制MockApi發送數據
getTestScheduler().flush();
+ fixture.detectChanges();
// 模擬點擊教師列表中的第二個教師
console.log(component.teachers.length);
```
此時我們將在V層中將查看到教師列表中的兩個教師。
### setValue()
最后調用`FormControl`的`setValue()`完成`option`的模擬點擊:
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
@@ -51,5 +51,7 @@ describe('KlassSelectComponent', () => {
console.log(component.teachers.length);
const teacher = component.teachers[1];
console.log(teacher);
+
+ component.teacherId.setValue(teacher.id);
});
});
panjie
```

## 其它方式
我們還可以在單元測試中使用代碼的形式來模擬option的點擊(實際上Angular在select中的option點擊做的并不好),但此方法使用的代碼較多,難度相對較大,實際在生產中我們也很少使用。在這我們僅給出相應的代碼供參考:
```typescript
fit('模擬點擊option', () => {
expect(component).toBeTruthy();
component.beChange
.subscribe((data: number) => console.log('接收到了彈出的數據', data));
// 手動控制MockApi發送數據
getTestScheduler().flush();
fixture.detectChanges();
// 獲取select
const htmlSelect = fixture.debugElement.query(By.css('select')).nativeElement as HTMLSelectElement;
htmlSelect.click();
// 獲取第二個選項
const htmlOption = htmlSelect.options[1];
// 用該選項的值設置select的值,從而實現模塊點擊
htmlSelect.value = htmlOption.value;
// 發送change事件,通知瀏覽器、angular、觀察者select值已經發生了變化
htmlSelect.dispatchEvent(new Event('change'));
});
```
## 上結
本結我們使用`getTestScheduler()`方法控制了`MockApi`返回值的時機。這使得我們在脫離后臺的開發中,即可以決定http請求返回什么值,又可以決定其值在什么時候返回。這有利于我們掌握組件在http請求前后的狀態。
我們還調用了`formControl`的`setValue()`方法來快速的模擬用戶的點擊,這為以后使用代碼來完全代替不可靠的人工點擊打下了基礎。
| 名稱 | 鏈接 |
| -------- | ------------------------------------------------------------ |
| 本節源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.2.3.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.2.3.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 發布部署
- 第九章 總結