# MockApi
**注意**:本节内容基于上节源码(带答案)构建 ,如果你发现与自己代码存在不一致情况,请参考上节答案完后作业后继续学习。
如果你独立完成了上一节的作业,那你真的真的是太厉害了。在计算机工程的路上继续走下去,相信不远你就可以小有所成。如果我们是在参考答案的情况下完成答案,那么也不要气馁,因为包括当年笔者在内大多数的同学都会是这种情况。
我们在日常的开发中,经常出现由于单元测试难写,所以完全抛弃单元测试的情况。笔者以前也是这样的。功能都实现了,感觉单元测试就是画蛇添足,费力不讨好。
实际上,我们完全可以借助单元测试,实现某个功能的独立开发。此处的独立,指可以脱离后台,脱离其它的逻辑。以新建班级为例:新建班级的前提是使用用户名密码来登录系统,登录系统则依赖于真实的后台。
在单元测试的支持,我们完全可以做到:1. 不需要提前登录。2.保存班级也不依赖于后台。这在实际的生产项目中显得尤其重要:
1. 如果前台的开发依赖于后台,则前台必须等待后台开发完毕后才能开工。而如果真是这样,那么前后台分离的意义又是什么呢?
2. 有些易变的用户,经常在看到**成品**后变更自己的需求。而如果我们的成品依赖于后台,则用户变更需求的时候就需求前后台全部改一便;而如果我们给用户看到的**成品**不需要后台支撑,是不是变更起来就更省时一些?
## MockApi
mock为模拟的意思,相信以后你会越来越多接触到此单词。脱离真实的后台的最佳方法便是按后台给出的Api规范对后台接口进行模拟。模拟的方式有很多,在这我们使用团队当前认为最简单有效的方式:拦截器。
![image-20210319092246668](https://img.kancloud.cn/5f/6f/5f6f45180b91ae01d8c7ecaa70b225a7_1422x488.png)
由图可以看出,使用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
```
实际上,我们通过刚才的命令引入了一个第三方的拦截器。我们可以像使用自己写的拦截器一样来使用它。该拦截器的实现原理如下:
![image-20210321100150405](https://img.kancloud.cn/69/e6/69e6edf29445afdc1f2b8e915f0b098b_1574x674.png)
### 新建测试文件
为了不影响原来的测试文件,我们在班级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. 修正错误后,控制台报错如下:
![image-20210319090537487](https://img.kancloud.cn/b6/fd/b6fdfb37bfa27cfb5471851c108e79a3_1590x94.png)
上述错错说明,在调用此时说明MockApi已生效。产生错误的原因是由于`ClazzMockApi`的`getInjectors()`方法返回了一个空数组,空数组说明其尚不具备模拟任何API的能力。那么返回错误信息也就理所当然了。
此时如若我们同时查看控制台中的网络信息,则发现并没有向真实的后台发起网络请求。这与我们前台讲过的MockApi拦截器相吻合,在当前模块仅有MockApi拦截器的情况下,原理图如下:
![image-20210319092246668](https://img.kancloud.cn/5f/6f/5f6f45180b91ae01d8c7ecaa70b225a7_1422x488.png)
## 建立模拟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`中的数据。
此时我们再次执行单元测试,控制台显示保存成功信息:
![image-20210319093210533](https://img.kancloud.cn/2b/00/2b00fe07a1d2cda0e0ecc17f151d2393_1244x198.png)
**注意**: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 发布部署
- 第九章 总结