本节来共同完成删除时弹窗功能。
首先按启动数据库、前后台、添加模拟数据的步骤进行一些数据准备:
![](https://img.kancloud.cn/1f/00/1f008fab3ed97c11223743f9b08a3881_1083x211.png)
# confirm
confirm是任何浏览器均支持的方法,用于做一些提示的功能。通过简单的代码来了解一下:
src/app/student/index/index.component.ts
```javascript
/**
* 删除学生
* @param student 学生
*/
onDelete(student: Student): void {
const result = confirm('这里是提示的消息'); // ➊
if (result) {
alert('用户点击了确认'); // ➋
} else {
alert('用户点击了取消');
}
alert('用户做出选择后,代码继续执行');
}
```
* ➊ 弹出确认框,提示内容:这里是提示的消息
* ➋ alert是浏览器弹窗的功能。与confirm一样都是比较古老的方法。
测试:
![](https://img.kancloud.cn/7e/88/7e883bd7922ea33a8834ef967aeb4e5b_810x377.gif)
观察测试结果总结出以下特点:
[] confirm执行时会中断代码的执行
[] 点击确认后将返回true
[] 点击取消后将返回false
# 功能开发
参考时序图,在C层中给出用户提示框后,若用户点击了"确认"则调用M层的`deleteById`方法,若点击"取消"则取消删除。
![](https://img.kancloud.cn/a4/16/a416a43526a68b4e7477617cde106449_721x491.png)
单元测试可以使用最少的成本来搭建起开发的环境,对于已经学习过的开发方法继续采用单元测试的方式进行功能相关的开发。按TDD的开发理论,先尝试写点测试的代码如下:
src/app/student/index/index.component.spec.ts
```javascript
fit('onDelete', () => {
// 替身及模似数据的准备
// 调用方法
// 断言
});
```
无论什么样的单元测试,基本上都是这个逻辑,先依据要测试方法的内部调用情况来准备替身和模拟数据,在此基本上再发请调用,最后进行断言以证明方法的执行符合预期。
我们一直说先Thinking,在Coding这也在单元测试中被充分的体现出来。因为如果没有充分的思索功能的实现步骤是无法动手写单元测试的。
在时序图中有一个条件判断,即:用户选择确认与选择取消是两条不同的线,本着细化测试粒度的原则,重新归划测试用例如下:
src/app/student/index/index.component.spec.ts
```javascript
fit('onDelete -> 取消删除', () => {
// 替身及模似数据的准备
// 调用方法
// 断言
});
fit('onDelete -> 确认删除', () => {
// 替身及模似数据的准备
// 调用方法
// 断言
});
```
## 取消删除
用户取消删除的操作较简单,当用户点击取消时,断言未进行M层删除方法的调用,同时也没有
src/app/student/index/index.component.spec.ts
```
fit('onDelete -> 取消删除', () => {
// 替身及模似数据的准备
const studentService: StudentService = TestBed.get(StudentService);
spyOn(studentService, 'deleteById');
spyOn(window, 'confirm').and.returnValue(false); // ➊
// 调用方法
component.onDelete(null);
// 断言
expect(studentService.deleteById).toHaveBeenCalledTimes(0);
});
```
* ➊ javascript是完全面向对象的语言。confirm方法只所以可以直接调用,根本的原因是由于其存在于对象`window`上
功能代码
测试结果:
![](https://img.kancloud.cn/35/20/35206f4ca0c929cf31a75cc9162bee60_432x112.png)
这是由于在此组件的测试过程中指定了使用`StudentStubService`来替代`StudentService`,而`StudentStubService`上并不存在`deleteById`方法。加入相应方法:
src/app/service/student-stub.service.ts
```
deleteById(id: number) {
}
```
再次运行单元测试,通过。
## 确认删除
确认删除的功能比取消删除要复杂一些,它不仅要向M层发请请求。还要在接收到M层操作成功的消息后在C层的数据中移除相应的`student`。
先断言要M层发请请求:
src/app/student/index/index.component.spec.ts
```
fit('onDelete -> 确认删除', () => {
// 替身及模似数据的准备
const studentService = TestBed.get(StudentService);
spyOn(studentService, 'deleteById');
spyOn(window, 'confirm').and.returnValue(true);
// 调用方法,删除第一个学生
const student = component.pageStudent.content[0];
component.onDelete(student);
// 断言
expect(studentService.deleteById).toHaveBeenCalledWith(student.id);
});
```
![](https://img.kancloud.cn/f4/0b/f40b7d5d1d45edaf3d5f1c0503a77473_641x76.png)
测试的结果符合预期,因为C层的代码还停留在一些提示功能上,补充功能如下:
src/app/student/index/index.component.ts
```javascript
/**
* 删除学生
* @param student 学生
*/
onDelete(student: Student): void {
const result = confirm('这里是提示的消息');
if (result) {
this.studentService.deleteById(student.id);
} else {
alert('用户点击了取消');
}
alert('用户做出选择后,代码继续执行 ');
}
```
![](https://img.kancloud.cn/fd/96/fd96a74a7b3e271d73ea4e0ac42933bf_360x168.png)
接着继续完成将删除的学生由组件C层数据中移除的操作。
src/app/student/index/index.component.spec.ts
```javascript
fit('onDelete -> 确认删除', () => {
...
// 断言
expect(studentService.deleteById).toHaveBeenCalledWith(student.id);
// 断言删除的学生成功的由前台移除
let found = false;
component.pageStudent.content.forEach(value => { // ➊
if (value === student) {
found = true;
}
});
expect(found).toBeFalsy();
});
```
* ➊ 遍历学生,断言找不到被删除掉的学生了
补充功能代码:
src/app/student/index/index.component.ts
```javascript
onDelete(student: Student): void {
const result = confirm('这里是提示的消息');
if (result) {
this.studentService.deleteById(student.id)
.subscribe(() => { // ➊
this.pageStudent.content.forEach((value, key) => {
if (value === student) {
this.pageStudent.content.splice(key, 1);
}
});
});
} else {
alert('用户点击了取消');
}
alert('用户做出选择后,代码继续执行 ');
}
```
* ➊ 实现**某个具有不确认定的操作完成以后**再执行其它操作的方法有两个:1 是使用承诺(promise);2是使用观察者(Observable)。在angular中广泛地使用了观察者替代了angularjs中的promise。
这里使用了`subscribe`,则要求`studentService.deleteById`方法的返回值为`Observable`。
src/app/service/student.service.ts
```javascript
deleteById(id: number): Observable<void> ➊{
return null;
}
```
* ➊ 执行成功后返回void(空值)
![](https://img.kancloud.cn/08/81/0881ad9dad4c811bdb856e69a9e49c67_962x145.png)
单元测试报错说:在测试文件的485行(你练习的代码可以不是485行,按提示对应找到相关行即可)发生了,在`undefined`类型上调用`subscribe`方法的错误,对应代码如下:
src/app/student/index/index.component.spec.ts
```javascript
component.onDelete(student);
```
此代码调用了`component.onDelete`方法,并没有调用`subscribe`方法的相关代码。所以必然不是本行代码出错,而是`component.onDelete`的方法在执行时发生了错误。`component.onDelete`中恰好存在以下代码:
src/app/student/index/index.component.ts
```javascript
this.studentService.deleteById(student.id)
.subscribe(() => {
```
也就是说此时`this.studentService.deleteById(student.id)`的返回值为`undefined`,所以才发生单元测试中报出的`TypeError: Cannot read property 'subscribe' of undefined`的错误。
这是由于在单元测试中,使用`spyOn(studentService, 'deleteById');`在设置`deleteById`的替身时没有为该替身设置返回值,此时默认的返回值便是undefined,近而引发了上述错误。为其设置返回值以解决问题:
src/app/student/index/index.component.spec.ts
```
import {BehaviorSubject} from 'rxjs';
...
const studentService = TestBed.get(StudentService);
const subject = new BehaviorSubject<void>(undefined); // ➊
spyOn(studentService, 'deleteById').and.returnValue(subject); // ➊
spyOn(window, 'confirm').and.returnValue(true);
```
* ➊ 区别于Subject,BehaviorSubject在初始化时可以装入一个值。由于此时的可观察者所携带的值的类型为void,所以此处传入undefined或null做为初始值
![](https://img.kancloud.cn/03/2a/032a19ceed6951d319f76521d0c2f9ff_298x191.png)
最后,去除或修正一些C层中测试的痕迹。
src/app/student/index/index.component.ts
```javascript
onDelete(student: Student): void {
const result = confirm('这里是提示的消息');
if (result) {
this.studentService.deleteById(student.id)
.subscribe(() => {
this.pageStudent.content.forEach((value, key) => {
if (value === student) {
this.pageStudent.content.splice(key, 1);
}
});
});
} else {
alert('用户点击了取消'); // ✘
console.log('用户点击了取消'); // ✚
}
alert('用户做出选择后,代码继续执行 '); // ✘
}
```
# M层
M层deleteById方法的开发主要参考相应的接口规范。主要功能点如下:
* 向地址/student/id发起请求
* 请求方式为get
* 返回值为可被观察者,该观察者携带的内容为`void`
src/app/service/student.service.spec.ts
```javascript
fit('deleteById', () => {
// 模拟数据及替身的准备
// 调用方法
// 断言发起了http请求
// 请求的方法为delete
// 返回值为可被观察者,该观察者携带的内容为`void`
});
```
尝试完成代码:
```javascript
fit('deleteById', () => {
// 模拟数据及替身的准备
// 调用方法
const id = Math.floor(Math.random() * 100);
let called = false;
service.deleteById(id).subscribe(() => {
called = true;
});
// 断言发起了http请求
const httpTestingController: HttpTestingController = TestBed.get(HttpTestingController);
const req = httpTestingController.expectOne(`http://localhost:8080/student/${id}`);
// 请求的方法为delete
expect(req.request.method).toEqual('DELETE');
// 返回值为可被观察者,该观察者携带的内容为`void`
expect(called).toBeFalsy();
req.flush(of());
expect(called).toBeTruthy();
});
```
## 完成功能代码
如果测试代码都难不倒我们的话,功能性的代码就更不会有问题了。
src/app/service/student.service.ts
```javascript
/**
* 删除学生
* @param id 学生id
*/
deleteById(id: number): Observable<void> {
const url = `http://localhost:8080/Student/${id}`;
return this.httpClient.delete<void>(url);
}
```
![](https://img.kancloud.cn/a9/07/a90796811ba2b1f4e680e8fa483f7b4e_869x94.png)
测试结果说:根本就没有向`http://localhost:8080/student/20"`这个地址发起请求......此时说明:要么测试代码错了,要么功能代码错了。经过排查确认,原来在测试代码中的请求地址被误输入为小写的`student`了,而正确的应该是大写的`Student`。有时候就这么一个小小的大小写问题也会引发大问题。单元测试与功能开发分别写一次,两次都写错误的概率要比一次写错的概率小多了。
修正如下:
src/app/service/student.service.spec.ts
```javascript
fit('deleteById', () => {
...
const req = httpTestingController.expectOne(`http://localhost:8080/student/${id}`); // ✘
const req = httpTestingController.expectOne(`http://localhost:8080/Student/${id}`); // ✚
...
});
```
再次测试,通过。
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.8.2](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.8.2) | - |
| confirm | [https://www.runoob.com/jsref/met-win-confirm.html](https://www.runoob.com/jsref/met-win-confirm.html) | 5 |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用