前后臺均啟用MVC的設計模式后則會發現前后臺的開發思想大同小異。
# Thinking
正式動手前先畫個圖:

在開發順序上,后臺的開發仍然建立由后向前來完成。這樣以避免在使用一些不熟悉的方法時帶來的對接問題。
# M層
按先接口、再實現類再單元測試的步驟依次進行開發。
## 接口
service/StudentService.java
```java
/**
* 刪除學生
* @param id 學生id
*/
void deleteById(Long id);
```
## 實現類
service/StudentServiceImpl.java
```java
@Override
public void deleteById(@NotNull Long id) {
Assert.notNull(id, "傳入的ID不能為NULL");
this.studentRepository.deleteById(id);
}
```
## 單元測試
service/StudentServiceImplTest
```java
/**
* 參數驗證
*/
@Test
public void deleteByIdValidate() {
}
/**
* 功能測試
*/
@Test
public void deleteById() {
// 替身及模擬返回值準備
// 調用方法
// 預測以期望的參數值調用了期望的方法
}
```
補充參數校驗代碼:傳入null時發生IllegalArgumentException異常。
service/StudentServiceImplTest
```java
@Test(expected = IllegalArgumentException.class)
public void deleteByIdValidate() {
this.studentService.deleteById(null);
}
```
補充功能測試代碼:
```java
@Test
public void deleteById() {
// 替身及模擬返回值準備
Long id = new Random().nextLong();
// studentRepository.deleteById方法的返回值類型為void。
// Mockito已默認為返回值為void默認生了返回值,無需對此替身單元做設置
// 調用方法
this.studentService.deleteById(id);
// 預測以期望的參數值調用了期望的方法
ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(this.studentRepository).deleteById(longArgumentCaptor.capture());
Assert.assertEquals(longArgumentCaptor.getValue(), id);
}
```
* 使用@MockBean注解的studentRepository,在返回值為void的方法中。Mockito已為其自動設置了調用時的返回值。
> Mockito在此對studentRepository默認執行了如下方法:Mockito.doNothing().when(this.studentRepository).deleteById(Mockito.anyLong());
# C層
相對于M層,C層還需要額外測試前臺調用此接口時的數據輸入與輸出是否符合預期。該接口返回狀態碼204,返回內容為空。
## 單元測試
按開發規范新建用例如下:
controller/StudentControllerTest.java
```java
@Test
public void deleteById() {
// 準備替身、傳入數據及返回數據
// 向指定的地址發起請求,并斷言返回狀態碼204
// 斷言調用方法符合預期
}
```
補充代碼:
```java
@Test
public void deleteById() throws Exception {
// 準備替身、傳入數據及返回數據
Long id = new Random().nextLong();
// studentService.deleteById方法返回類型為void,故無需對替身進行設置
// 向指定的地址發起請求,并斷言返回狀態碼204
String url = "/Student/" + id.toString();
this.mockMvc.perform(MockMvcRequestBuilders.delete(url))
.andExpect(MockMvcResultMatchers.status().is(204))
;
// 斷言調用方法符合預期
ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(this.studentService).deleteById(longArgumentCaptor.capture());
Assert.assertEquals(longArgumentCaptor.getValue(), id);
}
```
* 同M層的測試相同,studentService.deleteById的返回類型為void,同樣可以省略對替身做的設置。
## 功能代碼
按單元測試的提示補充代碼:
```
java.lang.AssertionError: Response status
Expected :204
Actual :405
```
上述錯誤提示:找到了請求的路徑`/Stduent/xxx`但并不是`delete`請求方法。
controller/StudentController.java
```java
@DeleteMapping("{id}")
public void deleteById() {
}
```
繼續進行單元測試
```
java.lang.AssertionError: Response status
Expected :204
Actual :200
<Click to see difference>
```
提示狀態碼返回了200,則修正如下:
```
@DeleteMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteById() {
}
```
繼續測試:
```
Wanted but not invoked:
studentServiceImpl bean.deleteById(
<Capturing argument>
);
-> at com.mengyunzhi.springbootstudy.controller.StudentControllerTest.deleteById(StudentControllerTest.java:304)
Actually, there were zero interactions with this mock.
```
出現一些不常的錯誤時,最好最有效的做法是:翻譯一下,猜猜它到底在說什么。
```
想調用但是卻沒有調用
studentServiceImpl bean.deleteById(
<Capturing argument 用于捕獲的參數>
);
-> 錯誤發生在單元測試的第304行:Mockito.verify(this.studentService).deleteById(longArgumentCaptor.capture());
實際上,在這個mock(指studentServiceImpl bean)就沒有產生影響 (沒有被調用過)
```
由于一些專用的單詞在上學期間并沒有接觸過,所以剛開始猜可能猜的不著邊際。不過沒關系,猜多了看到這個單詞的時候多了,結合看到這些單詞的情景,慢慢就猜準了。而這也應該是在英語學習中應該有境界。接受過基礎的教育的我們,英文的學習年限最少也有有6年,而且還是在記憶及學習能力都較優秀的青少年時代,但學習的成果好像還不如1-6周歲時對母語的學習。筆者猜想這是由于語言的學習天生需要"環境"的特性而決定的。
翻譯后發現報錯信息大概是說:這個(this.studentService)上的deleteById沒有如預期被調用。修正如下:
controller/StudentController.java
```java
@DeleteMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteById(Long id) {
this.studentService.deleteById(id);
}
```
繼續測試:
```
java.lang.AssertionError:
Expected :null
Actual :6076760375028858591
<Click to see difference>
...
at com.mengyunzhi.springbootstudy.controller.StudentControllerTest.deleteById(StudentControllerTest.java:305)
```
提示305行發生錯誤,期望接收的參數是6076760375028858591這個隨機數,但卻接收到了null,繼續修正:
controller/StudentController.java
```java
@DeleteMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteById(@PathVariable Long id) {
this.studentService.deleteById(id);
}
```
繼續測試后單元測試通過,功能完成。
在開發過程中往往會一次性的寫完自己能想到的功能性代碼,然后再結合測試來補充一些遺忘的功能性代碼或者修正一些單元測試及功能性代碼的錯誤。無論是寫單元測試還是功能性的代碼都是對業務邏輯的一次實現,對同一業務邏輯的兩次實現有效的地降低了一些在書寫時的低級錯誤。
# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.8.3](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.8.3) | - |
| mockito void methods | [https://www.baeldung.com/mockito-void-methods](https://www.baeldung.com/mockito-void-methods) | 10 |
- 序言
- 第一章: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
- 總結
- 開發規范
- 備用