# 数据验证
本节我们对新建班级时的班级名称进行非空验证,力求达到以下目标:
1. 模拟API模拟后台对数据进行验证。
2. 班级名称为空时,不允许点击提交按钮。
3. 未选择班主任时,不允许点击提交按钮。
### MockApi
一个好的MockApi往往会处理一些逻辑,特别是在数据的校验方面。而完成数据校验的前提是获取到这些数据。在第2小节中我们引入了能够模似后台API的MockApi,版本为`0.0.3`。本节中我们需要一些`0.0.5`版本的新特性,则需要对原依赖进行一些升级操作。
## 升级依赖
维护一个项目使其**不过时**是一种负责任的行为。相信在大家学习此教程时,执行`npm install`或多或少的都会看到一些依赖过时或有风险的提示:
```bash
found 2 moderate severity vulnerabilities
run `npm audit fix` to fix them, or `npm audit` for details
```
升级这些依赖的方法有很多,在此给出一种虽然笨但有效的方法。
我们知道`package.json`中控制着当前项目的所有依赖,那么我们当然可以简单的直接修改`package.json`中相关依赖的代码了。比如我们打开`package.json`将MockApi依赖由`0.0.3`版本修改为`0.0.5`版本。
```json
+++ b/first-app/package.json
@@ -20,7 +20,7 @@
"@angular/platform-browser-dynamic": "~11.0.9",
"@angular/router": "~11.0.9",
"@fortawesome/fontawesome-free": "^5.15.2",
- "@yunzhi/ng-mock-api": "0.0.3",
+ "@yunzhi/ng-mock-api": "0.0.5",
"bootstrap": "^4.6.0",
"jquery": "^3.5.1",
"popper.js": "^1.16.1",
```
然后在项目根目录中执行`npm install`来重新安装项目依赖,此时`@yunzhi/ng-mock-api`的版本便成功更新于`0.0.5`了。
使用`npm install`命令时会下载一些依赖至项目根目录的`node_modules`文件夹中。我们在其下可以找到一个`@yunzhi/ng-mock-api`文件夹。
```
panjie@panjies-Mac-Pro first-app % tree node_modules/@yunzhi -L 2
node_modules/@yunzhi
└── ng-mock-api
├── README.md
├── bundles
├── esm2015
├── fesm2015
├── lib
├── package.json 👈
├── public-api.d.ts
├── testing
├── yunzhi-ng-mock-api.d.ts
└── yunzhi-ng-mock-api.metadata.json
```
该文件夹中的`package.json`记录着当前依赖的具体信息,也包括版本号。
```json
"sideEffects": false,
"typings": "yunzhi-ng-mock-api.d.ts",
"version": "0.0.5" 👈
}
```
除此以外,还可以搜索`npm update package`或`npm update dependencies`来获取更多关于升级项目依赖的知识:
![image-20210319144803328](https://img.kancloud.cn/cb/68/cb684dc14cb076a97e990c43981b49d9_2094x184.png)
## 数据校验
创建MockApi时,我们将模拟的结果放到了`result`属性上:
```typescript
class ClazzMockApi implements MockApiInterface {
getInjectors(): ApiInjector<any>[] {
return [
{
method: 'POST',
url: 'clazz',
result: { 👈
id: 1,
name: '保存的班级名称',
createTime: 1234232,
teacher: {
id: 1,
name: '教师姓名'
}
}
}
];
}
}
```
`result`属性除可设置为一般的变量以外,还可以将其设置为函数:
```typescript
result: () => {
return {
id: 1,
name: '保存的班级名称',
createTime: 1234232,
teacher: {
id: 1,
name: '教师姓名'
}
};
}
```
这两种写法效果一致,运行单元测试我们当得到相同的结果:
![image-20210319093210533](https://img.kancloud.cn/2b/00/2b00fe07a1d2cda0e0ecc17f151d2393_1244x198.png)
但设置为函数时,我们在函数上指定相关参数,这些参数的值可以帮助我们实现更靠近其实环境的逻辑:
```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 {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
-import {ApiInjector, MockApiInterceptor, MockApiInterface} from '@yunzhi/ng-mock-api';
+import {ApiInjector, MockApiInterceptor, MockApiInterface, RequestOptions} from '@yunzhi/ng-mock-api';
import {FormsModule} from '@angular/forms';
describe('clazz add with mockapi', () => {
@@ -42,7 +42,7 @@ class ClazzMockApi implements MockApiInterface {
{
method: 'POST',
url: 'clazz',
- result: () => {
+ result: (urlMatches: string[], options: RequestOptions) => {
return {
id: 1,
name: '保存的班级名称',
```
其中`urlMatches`为请求URL的相关信息,`options`包括了请求主体、请求headers等信息。我们此时预实现请求的数据验证功能,则正好需要这个请求主体:
```typescript
result: (urlMatches: string[], options: RequestOptions) => {
+ console.log('接收到了数据请求,请求主体的内容为:', options.body);
```
![image-20210319151625649](https://img.kancloud.cn/9f/cf/9fcf4718544b388233896f5a643b66cb_1318x230.png)
数据接收到后:当数据符合要求,则返回数据;如果不符合要求,则抛出一个异常。
```typescript
+++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts
@@ -44,6 +44,15 @@ class ClazzMockApi implements MockApiInterface {
url: 'clazz',
result: (urlMatches: string[], options: RequestOptions) => {
console.log('接收到了数据请求,请求主体的内容为:', options.body);
+ const clazz = options.body;
+ if (!clazz.name || clazz.name === '') {
+ throw new Error('班级名称未定义或为空');
+ }
+
+ if (!clazz.teacher || !clazz.teacher.id) {
+ throw new Error('班主任ID未定义');
+ }
+
return {
id: 1,
name: '保存的班级名称',
```
该异常将导致新班级保存失败:
![image-20210319152607494](https://img.kancloud.cn/9f/07/9f0775ff90a336fd49d06dd61263ea2f_1032x106.png)
### 完善模拟数据
下面我们再对模拟返回数据进行修正,让其返回更**合理**的模拟数据:
```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 {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
-import {ApiInjector, MockApiInterceptor, MockApiInterface, RequestOptions} from '@yunzhi/ng-mock-api';
+import {ApiInjector, MockApiInterceptor, MockApiInterface, randomNumber, RequestOptions} from '@yunzhi/ng-mock-api';
import {FormsModule} from '@angular/forms';
describe('clazz add with mockapi', () => {
@@ -52,13 +52,13 @@ class ClazzMockApi implements MockApiInterface {
if (!clazz.teacher || !clazz.teacher.id) {
throw new Error('班主任ID未定义');
}
-
+
return {
- id: 1,
+ id: randomNumber(),
name: '保存的班级名称',
- createTime: 1234232,
+ createTime: new Date().getTime(),
teacher: {
- id: 1,
+ id: clazz.teacher.id,
name: '教师姓名'
}
};
```
使用`randomNumber()`来生成一个随机的数字作为返回班级的id,这更贴近于生产环境;`createTime`设置为当前的时间,使数据看起来更真实,而`teacher`的`id`使入接收到的`id`,也是必要的。这样一样,返回数据看起来就正常的多了。
此时,我们在测试提交的教师id时,在也不必拘泥于**张三**、**李四**对应的`id=1`,`id = 2`了。在单元测试中完全可以设置一个随机教师id:
```typescript
+++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts
@@ -29,6 +29,8 @@ describe('clazz add with mockapi', () => {
});
fit('在MockApi下完成组件测试Submit', () => {
+ component.clazz.name = '测试班级名称';
+ component.clazz.teacherId = randomNumber();
component.onSubmit();
});
});
```
如此一来,一个合格的后台API替身便诞生了:
![image-20210319153043698](https://img.kancloud.cn/4d/13/4d135c95ecc9cb5790067fa38a661dc6_1392x336.png)
## V层测试
在启用fixture的自动检测变更后,如果未输入班级名称或未选择教师就点击保存按钮的话,将在控制台得到相应的错误信息:
```typescript
+++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts
@@ -28,11 +28,15 @@ describe('clazz add with mockapi', () => {
fixture.detectChanges();
});
- fit('在MockApi下完成组件测试Submit', () => {
+ it('在MockApi下完成组件测试Submit', () => {
component.clazz.name = '测试班级名称';
component.clazz.teacherId = randomNumber();
component.onSubmit();
});
+
+ fit('should create', () => {
+ fixture.autoDetectChanges();
+ });
});
/**
```
![image-20210319153352871](https://img.kancloud.cn/72/c9/72c9461ad1ab9bd06a929eea1e4d8e7f_1582x570.png)
这几乎与真正的后台表示无异。
## disabled
Angular的FormsModule支持button标签的disabled属性,当此属性为true时按钮不可用,为false时按钮可用。我们可以利用此特性,实现名称为空、未选择班主任时的禁止提交功能。
```html
+++ b/first-app/src/app/clazz/add/add.component.html
@@ -19,7 +19,9 @@
</div>
<div class="mb-3 row">
<div class="col-sm-10 offset-2">
- <button class="btn btn-primary">保存</button>
+ <button class="btn btn-primary"
+ [disabled]="clazz.name === '' || clazz.teacherId === null">保存
+ </button>
</div>
</div>
</form>
```
此时,再启用fixture的自动检测变更后,V层中的按钮将只在班级名称不为空且选择了teacher后可用。
![image-20210319140010101](https://img.kancloud.cn/92/3b/923bc32f17ea0839cc9c00bdc2c9c9f8_1468x314.png)
填写班级名称,并选择班主任后:
![image-20210319140036645](https://img.kancloud.cn/75/6d/756d9107cdf13616182e4b7f283aaf61_1100x328.png)
在模拟后台API的数据校验以及V层按钮disabled的表现下,我们再也不怕客户不按照我们指定的使用方式来使用系统了。
## 本节作业
请实现以下效果:
1. 初始化时提示名称及班主任不能为空:
![image-20210319154000907](https://img.kancloud.cn/d7/aa/d7aa3b558ba78d7cbad593ba43db4f69_1478x328.png)
2. 填写完相应的内容后,提示消失:
![image-20210319140036645](https://img.kancloud.cn/75/6d/756d9107cdf13616182e4b7f283aaf61_1100x328.png)
| 名称 | 链接 |
| -------- | ------------------------------------------------------------ |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.1.4.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.1.4.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 发布部署
- 第九章 总结