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

由圖可以看出,使用MockApi攔截器后,以往向后臺(tái)發(fā)起的http請(qǐng)求將被直接攔斷,取而代之的是一個(gè)**MockApi功能模塊**,該模塊接收http請(qǐng)求并按請(qǐng)求做出相應(yīng)的響應(yīng)。這種模式可以使我們專(zhuān)注單元測(cè)試的功能,而不在必真正的后臺(tái)服務(wù)其它邏輯關(guān)系。以班級(jí)保存API為例:
在真實(shí)的后臺(tái)中,保存班級(jí)前用戶(hù)必須登錄系統(tǒng),這使得我們?cè)谝蕾?lài)于真實(shí)后臺(tái)的單元測(cè)試中,必須考慮到這個(gè)邏輯,否則保存班級(jí)的單元測(cè)試將無(wú)法進(jìn)行。
不僅如此,在保存班級(jí)的時(shí)候,我們還依賴(lài)于**教師**,也就是說(shuō)在測(cè)試保存班級(jí)前,我們必須確保后臺(tái)已有存在的**教師**。在教程中,我們出于易用性的考慮,**非常規(guī)**的內(nèi)置了**張三**、**李四**兩位教師,這才使我們?cè)跍y(cè)試時(shí)保存`{name: 'test', teacher: {id: 1}}`不報(bào)錯(cuò)。
相像下如果系統(tǒng)不內(nèi)置這兩個(gè)教師用戶(hù)呢?那么在測(cè)試保存班級(jí)前,我們需要先登錄系統(tǒng)、再新建教師、最后在新建班級(jí)。如果繼續(xù)發(fā)揮我們的想像,就會(huì)發(fā)現(xiàn)這種依賴(lài)于真實(shí)的后臺(tái)開(kāi)發(fā)會(huì)將我們帶入萬(wàn)劫不復(fù)的惡夢(mèng)中。比如我們開(kāi)發(fā)班級(jí)刪除功能,則需要:先登錄系統(tǒng)、再新建教師、再新建班級(jí)、最后測(cè)試班級(jí)刪除。真實(shí)的項(xiàng)目的依賴(lài)環(huán)境遠(yuǎn)要比這個(gè)復(fù)雜的多,以我們當(dāng)前項(xiàng)目為例,后面我們還會(huì)添加**學(xué)生管理**功能,添加學(xué)生時(shí)必須指定學(xué)生所在的班級(jí),那么如果我們想沒(méi)講一個(gè)刪除學(xué)生功能,則需要在單元測(cè)試中如下進(jìn)行:先登錄系統(tǒng)、再新建教師、再新建班級(jí)、再新建學(xué)生、最后刪除學(xué)生。
上述情況僅僅是當(dāng)前系統(tǒng)有3個(gè)實(shí)體(教師、班級(jí)、學(xué)生)下發(fā)生的,如果系統(tǒng)中有10個(gè)實(shí)體呢?100個(gè)呢?那就意味著單元測(cè)試無(wú)法進(jìn)行。即使你特別有耐心的按邏輯進(jìn)行了單元測(cè)試,但軟件的魅力在于**變化**,比如當(dāng)你所有的單元測(cè)試都進(jìn)行完畢后,甲方突然說(shuō)教師管理中需要增加一個(gè)**出生日期**字段,且該字段為必填。那么現(xiàn)在想像一樣自己的工作量吧。
我想以上原因可能是**單元測(cè)試**這個(gè)環(huán)節(jié)被廣大的程序員們忽略的原因之一吧。有了MockApi以后,我們?cè)僖膊槐乩頃?huì)后臺(tái)邏輯中復(fù)雜的難處理的關(guān)系了。
> 一個(gè)偉大的技術(shù)必然有其偉大之處,我們無(wú)法感知到它的偉大的原因往往是因?yàn)榱私獾牟粔?。單元測(cè)試便是這個(gè)偉大的技術(shù)之一。
### 引入第三方庫(kù)
我們通過(guò)引入第三方庫(kù)的形式來(lái)實(shí)現(xiàn)新建班級(jí)的Api ---- [Mock Api for Angular](https://www.npmjs.com/package/@yunzhi/ng-mock-api)
打開(kāi)控制臺(tái)并來(lái)到系統(tǒng)根目錄,執(zhí)行`npm install -s @yunzhi/ng-mock-api@0.0.3`:
**注意**:我們?cè)诖酥付ò姹咎?hào)的目的是為了統(tǒng)一大家學(xué)習(xí)與教程的版本號(hào),在生產(chǎn)項(xiàng)目中大多數(shù)時(shí)候都應(yīng)該使用`npm install -s @yunzhi/ng-mock-api`來(lái)安裝最新的版本。
```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
```
實(shí)際上,我們通過(guò)剛才的命令引入了一個(gè)第三方的攔截器。我們可以像使用自己寫(xiě)的攔截器一樣來(lái)使用它。該攔截器的實(shí)現(xiàn)原理如下:

### 新建測(cè)試文件
為了不影響原來(lái)的測(cè)試文件,我們?cè)诎嗉?jí)add組件所在文件夾,手動(dòng)新建一個(gè)`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
```
然后手動(dòng)初始化測(cè)試文件如下:
```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下完成組件測(cè)試Submit', () => {
});
});
```
接下來(lái)便可以像配置其它的攔截器一樣來(lái)配置此攔截器了:
```typescript
{provide: HTTP_INTERCEPTORS, multi: true, useClass: MockApiInterceptor}
```
此時(shí),攔截器便可以攔截所有的請(qǐng)求信息了。攔截請(qǐng)求僅僅是模擬API的前提,如若打造一個(gè)有效的模擬API,則還需要做到:為不同的請(qǐng)求返回不同的值,這時(shí)候便需要使用`MockApiInterceptor`的`forRoot()`方法來(lái)配置了。
> 大多數(shù)可配置的第三方`provider`都提供了 `forRoot()`用于接收配置信息。
### 初始化MockApi
按剛剛攔截器的思想,參考Mock Api for Angular文檔,如若模擬某個(gè)API,則需要經(jīng)過(guò)以下兩步:
1. 建立模擬接口的類(lèi)文件
2. 在MockApi加入攔截器,并在攔截器中引入建立的模擬接口類(lèi)文件
初始化用于提供模擬API的類(lèi)如下:
```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', () => {
});
});
+
+/**
+ * 班級(jí)模擬API
+ */
+class ClazzMockApi implements MockApiInterface {
+ getInjectors(): ApiInjector<any>[] {
+ return [];
+ }
+}
```
`ClazzMockApi`實(shí)現(xiàn)了`MockApiInterface`,以表明其是一個(gè)模擬Api的類(lèi),該類(lèi)的`getInjectors()`方法返回一個(gè)`ApiInjector`數(shù)組,這個(gè)數(shù)組中的每一項(xiàng)都可以模擬一個(gè)后臺(tái)API。
接下來(lái)添加MockApi攔截器,并調(diào)用`forRoot()`方法進(jìn)行配置:
```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攔截器中生效。
### 測(cè)試
此時(shí)在單元測(cè)試中測(cè)試組件的`onSubmit`方法,看看會(huì)發(fā)生什么:
```typescript
fit('在MockApi下完成組件測(cè)試Submit', () => {
+ component.onSubmit();
});
```
1. 控制臺(tái)中會(huì)報(bào)一個(gè)沒(méi)有`ngValue`解析器的錯(cuò)誤,請(qǐng)自行修正。
2. 修正錯(cuò)誤后,控制臺(tái)報(bào)錯(cuò)如下:

上述錯(cuò)錯(cuò)說(shuō)明,在調(diào)用此時(shí)說(shuō)明MockApi已生效。產(chǎn)生錯(cuò)誤的原因是由于`ClazzMockApi`的`getInjectors()`方法返回了一個(gè)空數(shù)組,空數(shù)組說(shuō)明其尚不具備模擬任何API的能力。那么返回錯(cuò)誤信息也就理所當(dāng)然了。
此時(shí)如若我們同時(shí)查看控制臺(tái)中的網(wǎng)絡(luò)信息,則發(fā)現(xiàn)并沒(méi)有向真實(shí)的后臺(tái)發(fā)起網(wǎng)絡(luò)請(qǐng)求。這與我們前臺(tái)講過(guò)的MockApi攔截器相吻合,在當(dāng)前模塊僅有MockApi攔截器的情況下,原理圖如下:

## 建立模擬API
接下來(lái),我們?cè)赻ClazzMockApi`的`getInjectors()`方法中添加第一個(gè)模似數(shù)據(jù):模擬新建班級(jí)。在正式編碼之前,我們還需要再次查看后臺(tái)為我們?cè)O(shè)定的API信息。這很重要,盡管我們不需要實(shí)現(xiàn)真正的班級(jí)保存功能,但卻需要保證模擬API完全符合真實(shí)后所定義的**規(guī)范**。
*****
新增班級(jí)的API為:
```bash
POST /clazz
```
| **類(lèi)型Type** | **名稱(chēng)Name** | **描述Description** | **類(lèi)型Schema** |
| :----------- | :----------- | :------------------ | :----------------------------------------------------------- |
| Body | clazz | 班級(jí) | `{name: string, teacher: {id: number}}` |
| Response | | 響應(yīng) | `{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: '保存的班級(jí)名稱(chēng)',
+ createTime: 1234232,
+ teacher: {
+ id: 1,
+ name: '教師姓名'
+ }
+ }
+ }
+ ];
}
}
```
由以上代碼可見(jiàn),我們?cè)赻getInjectors()`方法的返回?cái)?shù)據(jù)組中添加了一個(gè)對(duì)象。該對(duì)象由`method`、`url`以及`result`三個(gè)屬性組成,分別對(duì)就`請(qǐng)求方法`,`請(qǐng)求地址`以及`模擬返回的結(jié)果`。以此說(shuō)明:當(dāng)請(qǐng)求的地址與`url`相同,請(qǐng)求方法與`method`同時(shí)時(shí),返回`result`中的數(shù)據(jù)。
此時(shí)我們?cè)俅螆?zhí)行單元測(cè)試,控制臺(tái)顯示保存成功信息:

**注意**:MockApi在返回?cái)?shù)據(jù)時(shí)模擬了后臺(tái)的**延遲**,預(yù)使這個(gè)**延遲**反饋到組件上,需要保證僅有當(dāng)前測(cè)試用例在執(zhí)行。如果你尚不清楚為什么這么做,僅需要簡(jiǎn)單的重復(fù)學(xué)習(xí)下5.1小節(jié)的內(nèi)容。
MockApi是生產(chǎn)項(xiàng)目中不可或缺的部分。在團(tuán)隊(duì)的生產(chǎn)項(xiàng)目中,我們的開(kāi)發(fā)順序往往是先前臺(tái)、再后臺(tái)。這樣做可以避免很多在前期想像不到的錯(cuò)誤,同時(shí)也有利于后臺(tái)成員對(duì)整個(gè)項(xiàng)目的理解,盡而少犯一些錯(cuò)誤,降低前后臺(tái)的溝通成本。
## 本節(jié)作業(yè)
請(qǐng)比較`add.component.mock-api.spec.ts`、`add.component.spec.ts`兩個(gè)測(cè)試文件中對(duì)組件`onSubmit`的測(cè)試方法。你更愿意使用哪一種?為什么?
| 名稱(chēng) | 鏈接 |
| -------------------- | ------------------------------------------------------------ |
| Mock Api for Angular | [https://www.npmjs.com/package/@yunzhi/ng-mock-api](https://www.npmjs.com/package/@yunzhi/ng-mock-api) |
| 本節(jié)源碼 | [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 環(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 模塊與依賴(lài)注入
- 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é)
- 第三章 用戶(hù)登錄
- 3.1 初識(shí)單元測(cè)試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類(lèi)
- 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é)