# MockApi
**注意**:本節內容基于上節源碼(帶答案)構建 ,如果你發現與自己代碼存在不一致情況,請參考上節答案完后作業后繼續學習。
如果你獨立完成了上一節的作業,那你真的真的是太厲害了。在計算機工程的路上繼續走下去,相信不遠你就可以小有所成。如果我們是在參考答案的情況下完成答案,那么也不要氣餒,因為包括當年筆者在內大多數的同學都會是這種情況。
我們在日常的開發中,經常出現由于單元測試難寫,所以完全拋棄單元測試的情況。筆者以前也是這樣的。功能都實現了,感覺單元測試就是畫蛇添足,費力不討好。
實際上,我們完全可以借助單元測試,實現某個功能的獨立開發。此處的獨立,指可以脫離后臺,脫離其它的邏輯。以新建班級為例:新建班級的前提是使用用戶名密碼來登錄系統,登錄系統則依賴于真實的后臺。
在單元測試的支持,我們完全可以做到:1. 不需要提前登錄。2.保存班級也不依賴于后臺。這在實際的生產項目中顯得尤其重要:
1. 如果前臺的開發依賴于后臺,則前臺必須等待后臺開發完畢后才能開工。而如果真是這樣,那么前后臺分離的意義又是什么呢?
2. 有些易變的用戶,經常在看到**成品**后變更自己的需求。而如果我們的成品依賴于后臺,則用戶變更需求的時候就需求前后臺全部改一便;而如果我們給用戶看到的**成品**不需要后臺支撐,是不是變更起來就更省時一些?
## MockApi
mock為模擬的意思,相信以后你會越來越多接觸到此單詞。脫離真實的后臺的最佳方法便是按后臺給出的Api規范對后臺接口進行模擬。模擬的方式有很多,在這我們使用團隊當前認為最簡單有效的方式:攔截器。

由圖可以看出,使用MockApi攔截器后,以往向后臺發起的http請求將被直接攔斷,取而代之的是一個**MockApi功能模塊**,該模塊接收http請求并按請求做出相應的響應。這種模式可以使我們專注單元測試的功能,而不在必真正的后臺服務其它邏輯關系。以班級保存API為例:
在真實的后臺中,保存班級前用戶必須登錄系統,這使得我們在依賴于真實后臺的單元測試中,必須考慮到這個邏輯,否則保存班級的單元測試將無法進行。
不僅如此,在保存班級的時候,我們還依賴于**教師**,也就是說在測試保存班級前,我們必須確保后臺已有存在的**教師**。在教程中,我們出于易用性的考慮,**非常規**的內置了**張三**、**李四**兩位教師,這才使我們在測試時保存`{name: 'test', teacher: {id: 1}}`不報錯。
相像下如果系統不內置這兩個教師用戶呢?那么在測試保存班級前,我們需要先登錄系統、再新建教師、最后在新建班級。如果繼續發揮我們的想像,就會發現這種依賴于真實的后臺開發會將我們帶入萬劫不復的惡夢中。比如我們開發班級刪除功能,則需要:先登錄系統、再新建教師、再新建班級、最后測試班級刪除。真實的項目的依賴環境遠要比這個復雜的多,以我們當前項目為例,后面我們還會添加**學生管理**功能,添加學生時必須指定學生所在的班級,那么如果我們想沒講一個刪除學生功能,則需要在單元測試中如下進行:先登錄系統、再新建教師、再新建班級、再新建學生、最后刪除學生。
上述情況僅僅是當前系統有3個實體(教師、班級、學生)下發生的,如果系統中有10個實體呢?100個呢?那就意味著單元測試無法進行。即使你特別有耐心的按邏輯進行了單元測試,但軟件的魅力在于**變化**,比如當你所有的單元測試都進行完畢后,甲方突然說教師管理中需要增加一個**出生日期**字段,且該字段為必填。那么現在想像一樣自己的工作量吧。
我想以上原因可能是**單元測試**這個環節被廣大的程序員們忽略的原因之一吧。有了MockApi以后,我們再也不必理會后臺邏輯中復雜的難處理的關系了。
> 一個偉大的技術必然有其偉大之處,我們無法感知到它的偉大的原因往往是因為了解的不夠。單元測試便是這個偉大的技術之一。
### 引入第三方庫
我們通過引入第三方庫的形式來實現新建班級的Api ---- [Mock Api for Angular](https://www.npmjs.com/package/@yunzhi/ng-mock-api)
打開控制臺并來到系統根目錄,執行`npm install -s @yunzhi/ng-mock-api@0.0.3`:
**注意**:我們在此指定版本號的目的是為了統一大家學習與教程的版本號,在生產項目中大多數時候都應該使用`npm install -s @yunzhi/ng-mock-api`來安裝最新的版本。
```bash
panjie@panjies-Mac-Pro first-app % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app
panjie@panjies-Mac-Pro first-app % npm install -s @yunzhi/ng-mock-api@0.0.3
+ @yunzhi/ng-mock-api@0.0.3
added 1 package, removed 1 package and audited 1471 packages in 10.284s
```
實際上,我們通過剛才的命令引入了一個第三方的攔截器。我們可以像使用自己寫的攔截器一樣來使用它。該攔截器的實現原理如下:

### 新建測試文件
為了不影響原來的測試文件,我們在班級add組件所在文件夾,手動新建一個`add.component.mock-api.spec.ts`。
```bash
panjie@panjies-Mac-Pro add % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/clazz/add
panjie@panjies-Mac-Pro add % tree
.
├── add.component.css
├── add.component.html
├── add.component.mock-api.spec.ts ??
├── add.component.spec.ts
└── add.component.ts
0 directories, 5 files
```
然后手動初始化測試文件如下:
```typescript
import {AddComponent} from './add.component';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HttpClientModule} from '@angular/common/http';
describe('clazz add with mockapi', () => {
let component: AddComponent;
let fixture: ComponentFixture<AddComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddComponent],
imports: [HttpClientModule]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
fit('在MockApi下完成組件測試Submit', () => {
});
});
```
接下來便可以像配置其它的攔截器一樣來配置此攔截器了:
```typescript
{provide: HTTP_INTERCEPTORS, multi: true, useClass: MockApiInterceptor}
```
此時,攔截器便可以攔截所有的請求信息了。攔截請求僅僅是模擬API的前提,如若打造一個有效的模擬API,則還需要做到:為不同的請求返回不同的值,這時候便需要使用`MockApiInterceptor`的`forRoot()`方法來配置了。
> 大多數可配置的第三方`provider`都提供了 `forRoot()`用于接收配置信息。
### 初始化MockApi
按剛剛攔截器的思想,參考Mock Api for Angular文檔,如若模擬某個API,則需要經過以下兩步:
1. 建立模擬接口的類文件
2. 在MockApi加入攔截器,并在攔截器中引入建立的模擬接口類文件
初始化用于提供模擬API的類如下:
```typescript
+++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts
@@ -1,6 +1,7 @@
import {AddComponent} from './add.component';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HttpClientModule} from '@angular/common/http';
+import {ApiInjector, MockApiInterface} from '@yunzhi/ng-mock-api';
describe('clazz add with mockapi', () => {
let component: AddComponent;
@@ -23,3 +24,12 @@ describe('clazz add with mockapi', () => {
});
});
+
+/**
+ * 班級模擬API
+ */
+class ClazzMockApi implements MockApiInterface {
+ getInjectors(): ApiInjector<any>[] {
+ return [];
+ }
+}
```
`ClazzMockApi`實現了`MockApiInterface`,以表明其是一個模擬Api的類,該類的`getInjectors()`方法返回一個`ApiInjector`數組,這個數組中的每一項都可以模擬一個后臺API。
接下來添加MockApi攔截器,并調用`forRoot()`方法進行配置:
```typescript
+++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts
@@ -1,7 +1,7 @@
import {AddComponent} from './add.component';
import {ComponentFixture, TestBed} from '@angular/core/testing';
-import {HttpClientModule} from '@angular/common/http';
-import {ApiInjector, MockApiInterface} from '@yunzhi/ng-mock-api';
+import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
+import {ApiInjector, MockApiInterceptor, MockApiInterface} from '@yunzhi/ng-mock-api';
describe('clazz add with mockapi', () => {
let component: AddComponent;
@@ -10,7 +10,14 @@ describe('clazz add with mockapi', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddComponent],
- imports: [HttpClientModule]
+ imports: [HttpClientModule],
+ providers: [
+ {
+ provide: HTTP_INTERCEPTORS,
+ multi: true,
+ useClass: MockApiInterceptor.forRoot([ClazzMockApi])
+ }
+ ]
}).compileComponents();
});
```
上述代碼在`forRoot`方法中傳入`ClazzMockApi`,這使得`ClazzMockApi`在該模擬API攔截器中生效。
### 測試
此時在單元測試中測試組件的`onSubmit`方法,看看會發生什么:
```typescript
fit('在MockApi下完成組件測試Submit', () => {
+ component.onSubmit();
});
```
1. 控制臺中會報一個沒有`ngValue`解析器的錯誤,請自行修正。
2. 修正錯誤后,控制臺報錯如下:

上述錯錯說明,在調用此時說明MockApi已生效。產生錯誤的原因是由于`ClazzMockApi`的`getInjectors()`方法返回了一個空數組,空數組說明其尚不具備模擬任何API的能力。那么返回錯誤信息也就理所當然了。
此時如若我們同時查看控制臺中的網絡信息,則發現并沒有向真實的后臺發起網絡請求。這與我們前臺講過的MockApi攔截器相吻合,在當前模塊僅有MockApi攔截器的情況下,原理圖如下:

## 建立模擬API
接下來,我們在`ClazzMockApi`的`getInjectors()`方法中添加第一個模似數據:模擬新建班級。在正式編碼之前,我們還需要再次查看后臺為我們設定的API信息。這很重要,盡管我們不需要實現真正的班級保存功能,但卻需要保證模擬API完全符合真實后所定義的**規范**。
*****
新增班級的API為:
```bash
POST /clazz
```
| **類型Type** | **名稱Name** | **描述Description** | **類型Schema** |
| :----------- | :----------- | :------------------ | :----------------------------------------------------------- |
| Body | clazz | 班級 | `{name: string, teacher: {id: number}}` |
| Response | | 響應 | `{id: number, name: string, createTime: number, teacher: {id: number, name: string}}` |
*****
```typescript
+++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts
@@ -3,6 +3,7 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {ApiInjector, MockApiInterceptor, MockApiInterface} from '@yunzhi/ng-mock-api';
import {FormsModule} from '@angular/forms';
+import {RequestMethodType} from '@yunzhi/ng-mock-api/lib/mock-api.types';
describe('clazz add with mockapi', () => {
let component: AddComponent;
@@ -38,6 +39,20 @@ describe('clazz add with mockapi', () => {
*/
class ClazzMockApi implements MockApiInterface {
getInjectors(): ApiInjector<any>[] {
- return [];
+ return [
+ {
+ method: 'POST',
+ url: 'http://angular.api.codedemo.club:81/clazz',
+ result: {
+ id: 1,
+ name: '保存的班級名稱',
+ createTime: 1234232,
+ teacher: {
+ id: 1,
+ name: '教師姓名'
+ }
+ }
+ }
+ ];
}
}
```
由以上代碼可見,我們在`getInjectors()`方法的返回數據組中添加了一個對象。該對象由`method`、`url`以及`result`三個屬性組成,分別對就`請求方法`,`請求地址`以及`模擬返回的結果`。以此說明:當請求的地址與`url`相同,請求方法與`method`同時時,返回`result`中的數據。
此時我們再次執行單元測試,控制臺顯示保存成功信息:

**注意**:MockApi在返回數據時模擬了后臺的**延遲**,預使這個**延遲**反饋到組件上,需要保證僅有當前測試用例在執行。如果你尚不清楚為什么這么做,僅需要簡單的重復學習下5.1小節的內容。
MockApi是生產項目中不可或缺的部分。在團隊的生產項目中,我們的開發順序往往是先前臺、再后臺。這樣做可以避免很多在前期想像不到的錯誤,同時也有利于后臺成員對整個項目的理解,盡而少犯一些錯誤,降低前后臺的溝通成本。
## 本節作業
請比較`add.component.mock-api.spec.ts`、`add.component.spec.ts`兩個測試文件中對組件`onSubmit`的測試方法。你更愿意使用哪一種?為什么?
| 名稱 | 鏈接 |
| -------------------- | ------------------------------------------------------------ |
| Mock Api for Angular | [https://www.npmjs.com/package/@yunzhi/ng-mock-api](https://www.npmjs.com/package/@yunzhi/ng-mock-api) |
| 本節源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.1.2.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.1.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 發布部署
- 第九章 總結