同其它组件一样,首先我们初始化原型:
```bash
panjiedeMacBook-Pro:clazz panjie$ ng g c edit
CREATE src/app/clazz/edit/edit.component.css (0 bytes)
CREATE src/app/clazz/edit/edit.component.html (19 bytes)
CREATE src/app/clazz/edit/edit.component.spec.ts (612 bytes)
CREATE src/app/clazz/edit/edit.component.ts (267 bytes)
UPDATE src/app/clazz/clazz.module.ts (663 bytes)
```
## V层
班级新建组件中引入了教师选择列表组件,此时在原型初始化中我们仍然引用该组件以达到快速初始化原型的目的:
```html
<form class="container-sm">
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">名称</label>
<div class="col-sm-10">
<input type="text" class="form-control">
<small class="text-danger">
班级名称不能为空
</small>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">班主任</label>
<div class="col-sm-10">
<app-klass-select></app-klass-select>
<small class="text-danger">
必须指定一个班主任
</small>
</div>
</div>
<div class="mb-3 row">
<div class="col-sm-10 offset-2">
<button class="btn btn-primary">保存
</button>
</div>
</div>
</form>
```
然后在`src/app/clazz/edit/edit.component.spec.ts`下的动态测试模块中加入`app-klass-select`对应的组件`KlassSelectComponent`:
```typescript
await TestBed.configureTestingModule({
declarations: [EditComponent, KlassSelectComponent]
})
```
最后在单元测试的测试用例上加入`fit`,启用`ng t`来查看原型:
![image-20210401150902012](https://img.kancloud.cn/a0/8f/a08fe878473e4f0f5b2e8e97b735f8da_1608x154.png)
由于`KlassSelectComponent`依赖于后台的数据,在未引入相关的模块和MockApi拦截器前,进行单元测试时将触发上述错误。该错误在教师新增组件的测试中已经出现过一次,请尝试自行解决。
## 图解原理
随着项目的增大、组件间的关系将会逐渐加强。这将会使我们的测试变得越发不可控制。假设A组件当前仅仅依赖于`AMockApi`,接着我们又开发了100个依赖于组件A的新组件,在测试时我们分别在100个动态测试模块的拦截器上加入了``AMockApi``:
```typescript
providers: [
{
provide: HTTP_INTERCEPTORS,
multi: true,
useClass: MockApiInterceptor.forRoot([AMockApi])
}
]
```
然后正常的事情发生了:由于需求的变更,A组件增加了新功能,新功能添加完成后,不但要依赖于`AMockApi`,还依赖于`BMockApi`。
想像一下当A组件的功能功能后,使用`ng t`来测试其它100个组件时,每个组件都需要加入到`BMockApi`的依赖:
```typescript
providers: [
{
provide: HTTP_INTERCEPTORS,
multi: true,
useClass: MockApiInterceptor.forRoot([AMockApi, BMockApi])
}
]
```
在Angular中,可以建立一个专门用于模拟后台的模块,在该模块中加入所有的`MockApi`来模拟整个后台。然后再其它需要使用模拟后台的测试模块中,仅仅引入这个专门的模块即可。以前测试模块为例,按照传统的解决办法,若想成功创建教师编辑组件,则应该在`imports`中加入`HttpClientModule`:
```typescript
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [EditComponent, KlassSelectComponent],
imports: [
HttpClientModule
]
})
.compileComponents();
});
```
原理图对应如下:
![image-20210401154227192](https://img.kancloud.cn/e0/ce/e0cea73195649747bcc6800ebf4a8b87_1690x546.png)
而如果想使用`MockApi`来替代真实的后台Api,则需要在`provider`中提供`HTTP_INTERCEPTORS`:
```typescript
await TestBed.configureTestingModule({
declarations: [EditComponent, KlassSelectComponent],
imports: [
HttpClientModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
multi: true,
useClass: MockApiInterceptor.forRoot([])
}
]
})
.compileComponents();
```
此时`MockApi`请作用的原因是由于`HttpClient`在发起数据请求以前,会查看当前模块中提供的`HTTP_INTERCEPTORS`,如果当前模块提供了`HTTP_INTERCEPTORS`则会调用第一个`HTTP_INTERCEPTOR`,然后由第一个调用第二个,依次累推。
![image-20210401155231459](https://img.kancloud.cn/3c/3b/3c3baf656b03d0fd4320453fb7b77ff8_1212x612.png)
但`MockApi`不有按常规出牌,当调用到它时,它没有继续调用下一个拦截器,而是直接自己用模拟数据回应了:
![image-20210401155426811](https://img.kancloud.cn/7b/b1/7bb107384234a13913ab95e64d929abf_1126x594.png)
这就是在加入`MockApiInterceptor.forRoot()`拦截器后模拟API起作用的原因。
而`MockApiInterceptor`是根据`forRoot()`方法中传入的数组来返回对应的模拟数据的,所以若想某些模拟数据生效,是需要加相应的类加入到`forRoot()`方法中:
```typescript
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [EditComponent, KlassSelectComponent],
imports: [
HttpClientModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
multi: true,
useClass: MockApiInterceptor.forRoot([TeacherMockApi])
}
]
})
.compileComponents();
```
以上我们再一次解释了引用`HttpModule`以及`MockApi`的原理。
## 公共测试模块
我们添加一个公共测试模块的话,原理图如下:
![image-20210401160603834](https://img.kancloud.cn/c7/6c/c76c2dffd822b5f4e738ed241cb581e1_1672x1084.png)
我们将提供`HttpClient`的`HttpModule`以及提供拦截器的`MockApiInterceptor`添加到新的模块中,然后在动态测试模块中引用这个新模块。这样以来,将动态测试模块需要`httpClient`时,则会由其`imports`的**专用用于模拟全部后台Api的模块**中的`HttpModule`中引入;当`httpClient`发起请求时,也会由**专用用于模拟全部后台Api的模块**中查找`HTTP_INTERCEPTORS`。
下面,让我们共同来实现上述原理图。
### 建立模块
该模块的作用是为单元测试中的动态测试模块提供MockApi功能,将其命名为`MockApiTestingModule`,并将其建立在`mock-api`下是个不错的选择:
```bash
panjie@panjie-de-Mac-Pro mock-api % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/mock-api
panjie@panjie-de-Mac-Pro mock-api % ng g m MockApiTesting --flat
CREATE src/app/mock-api/mock-api-testing.module.ts (200 bytes)
```
**注意:** 上述命令使用了`--flat` 参数,表示把文件创建到当前文件夹,如果不加该参数,`ng`将会当前文件夹下创建一个`mock-api-testing`文件夹。
此时`mock-api`文件夹下共存在3个文件:
```bash
CREATE src/app/mock-api/mock-api-testing.module.ts (200 bytes)
panjie@panjie-de-Mac-Pro mock-api % tree
.
├── clazz.mock.api.ts
├── mock-api-testing.module.ts
└── teacher.mock.api.ts
0 directories, 3 files
```
然后打开`mock-api-testing.module.ts`,为其添加对`HttpModule`的依赖,以及提供MockApi拦截器:
```typescript
+++ b/first-app/src/app/mock-api/mock-api-testing.module.ts
@@ -1,11 +1,20 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
+import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
+import {MockApiTestingInterceptor} from '@yunzhi/ng-mock-api/testing';
@NgModule({
declarations: [],
imports: [
+ HttpClientModule,
CommonModule
+ ],
+ providers: [
+ {
+ provide: HTTP_INTERCEPTORS, multi: true,
+ useClass: MockApiTestingInterceptor.forRoot([])
+ }
]
})
export class MockApiTestingModule {
```
接下来将所有的MockApi文件,添加到`forRoot([])`方法中的数组中:
```typescript
- useClass: MockApiTestingInterceptor.forRoot([])
+ useClass: MockApiTestingInterceptor.forRoot([
+ ClazzMockApi,
+ TeacherMockApi
+ ])
```
## 应用模块
现在可以在相关的测试中引用`MockApiTestingModule`来快速的满足组件的各种后台请求了,打开班级编辑组件对应的测试文件,如此配置动态测试模块:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.spec.ts
import {MockApiTestingModule} from '../../mock-api/mock-api-testing.module';
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [EditComponent, KlassSelectComponent],
imports: [
MockApiTestingModule
]
})
.compileComponents();
});
```
此时再查看`ng t`,在控制台出现了找不到`formControl`的异常。
![image-20210402092204041](https://img.kancloud.cn/7f/bc/7fbcf4e29250f4d28e0100a27ef45554_1206x92.png)
这是由于教师选择组件中使用了响应式表单,而当前动态测试模块并没有引入响应式表单模块,所以提示无法识别`formControl`的错误。解决的方法是在当前动态测试模块中引用响应式表单模块:
```typescript
- MockApiTestingModule
+ MockApiTestingModule,
+ ReactiveFormsModule
```
此时控制台错误消息。错误虽然消失了,但当前方案仍存在一定的问题。这是由于我们在解决该问题时,仍然需要考虑子组件对`ReactiveFormsModule`的依赖问题,如果子组件依赖于多个模块,我们同样还需要在当前的单元测试中引入其它多个模块。所以从本质上来,我们仍然面临着某个子组件进行更新后,引用它的父组件需要全部在`imports`中增加一遍对应模块的问题。带着这个问题继续学习,我们将在后面的章节中同样使用抽离模块的方法来解决该问题。
最后,由于在`MockApiTestingModule`中使用的拦截器是`MockApiTestingInterceptor`而非`MockApiInterceptor`,所以我们需要在测试代码中手动触发数据返回,同时启用变更检测。
```typescript
fit('should create', () => {
expect(component).toBeTruthy();
+ getTestScheduler().flush();
+ fixture.autoDetectChanges();
});
```
最终效果如下:
![image-20210402093456159](https://img.kancloud.cn/48/cd/48cdff073cc4185dc865b053300ea27e_1116x414.png)
至此,基于教师选择组件的班级编辑组件原型初始化完毕。
| 名称 | 链接 |
| ---------- | ------------------------------------------------------------ |
| 定义提供者 | [https://angular.cn/guide/dependency-injection-providers#defining-providers](https://angular.cn/guide/dependency-injection-providers#defining-providers) |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.4.1.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.4.1.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 发布部署
- 第九章 总结