本节来共同完成删除时弹窗功能。 首先按启动数据库、前后台、添加模拟数据的步骤进行一些数据准备: ![](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 |