本節來共同完成刪除時彈窗功能。
首先按啟動數據庫、前后臺、添加模擬數據的步驟進行一些數據準備:

# 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一樣都是比較古老的方法。
測試:

觀察測試結果總結出以下特點:
[] confirm執行時會中斷代碼的執行
[] 點擊確認后將返回true
[] 點擊取消后將返回false
# 功能開發
參考時序圖,在C層中給出用戶提示框后,若用戶點擊了"確認"則調用M層的`deleteById`方法,若點擊"取消"則取消刪除。

單元測試可以使用最少的成本來搭建起開發的環境,對于已經學習過的開發方法繼續采用單元測試的方式進行功能相關的開發。按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`上
功能代碼
測試結果:

這是由于在此組件的測試過程中指定了使用`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);
});
```

測試的結果符合預期,因為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('用戶做出選擇后,代碼繼續執行 ');
}
```

接著繼續完成將刪除的學生由組件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(空值)

單元測試報錯說:在測試文件的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做為初始值

最后,去除或修正一些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);
}
```

測試結果說:根本就沒有向`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
- 總結
- 開發規范
- 備用