前臺準備完畢后。接下來進行后臺的對接。后臺對接主要實現兩個接口:1. 根據ID獲取某位學生信息接口。 2. 更新某個學生信息的接口。接口規范在[本章節](http://www.hmoore.net/yunzhiclub/springboot_angular_guide/1378718)開始時已經給出。
前臺的主體編輯功能完成后,再來觀察相應的更新接口。此時發現接口的返回值并不符合前臺的數據要求:
前臺對編輯的返回值處理如下:
```javascript
/**
* 更新學生
* @param id id
* @param student 學生
*/
update(id: number, student: Student): Observable<Student?> {
const url = `http://localhost:8080/Student/${id}`;
return this.httpClient.put<Student>(url, student);
}
```
* ? 更新接口應該返回學生
但前期定義的接口規范返回的卻是空內容:

可以預見的是:如果按前面規定的接口規范開發,那么后臺的更新接口將無法滿足前臺的功能需求。若想滿足前臺的功能的需求,則需要變更后臺接口相應的返回值。由于此接口有了返回值,狀態碼也應該由204變更為200。
新的接口返回值如下:
#### 響應(返回值)Responses
| HTTP Code | Description | Schema |
| --- | --- | --- |
| **200** | 學生 | Student |
在相應功能的開發過程中,我們優先選擇開發前臺,然后再開發后臺的開發步驟也是基于這種接口規范可能會變更的現實。這種開發步驟能夠有效的避免一些接口定義無法滿足前臺實際需求的情況。先開發前臺再開發后臺,變更接口規范對后臺造成的影響最小。但如果先開發后臺再開發前臺就完全不一樣了。后臺開發完成后,前臺在使用的過程中發現適用有問題,此時就需要后臺進行修改來適應前臺。有時候這種修改會直接推翻后臺的邏輯性,使原后臺開發的接口的價值為0甚至為負值。
# 接口開發
先Thinking,再Coding:

# GetById
按時序圖的反方向進行初始化:
數據倉庫層由于繼承了Crud接口,save方法已經由該接口提供了,所以直接忽略。
服務層初始化:service/StudentService.java
```
public interface StudentService {
...java
/**
* 查找學生
* @param id 學生ID
* @return 學生
*/
Student findById(@NotNull Long id);
```
服務層初始化:service/StudentServiceImpl.java
```java
public class StudentServiceImpl implements StudentService {
...
@Override
public Student findById(@NotNull Long id) {
return null;
}
```
C層初始化:controller/StudentController.java
```java
public class StudentController {
...
/**
* 通過ID查詢學生
* @param id 學生ID
* @return 學生
*/
public Student getById(Long id) {
return this.studentService.findById(id);
}
```
> 回看KlassController中的獲取某個班級時,會發現其方法名命名為:get;但此處被命名為getById。當某個方法參數較少時,可以采用`xxxBy參數a參數b`的形式來進行命名,當參數較多時,則一般直接命名為xxx。
此時在常規的開發方法中,便可以啟用postman或是直接啟動前開來進行功能開發了。在教程中對于已經學習過的知識點,我們仍然優先使用單元測試的方法進行功能開發。
## 功能開發
功能開發過程仍然按從后到前的開發步驟,在單元測試還沒有并熟練掌握前,這可以更好的支持傳統的測試方法。
### M層
service/StudentServiceImpl.java
```java
public class StudentServiceImpl implements StudentService {
...
@Override
public Student findById(@NotNull Long id) {
Assert.notNull(id, "id不能為null"); ?
return this.studentRepository.findById(id).get(); ?
}
```
* ? 非null校驗,當傳入null時,直接拋出異常并附帶提示信息
* ? 調用倉庫層返回學生
### M層單元測試
單元測試的過程中,如果單元測試的代碼過長或邏輯過于復雜,應該想辦法進行拆分,將測試粒度變小。本方法的測試邏輯相對簡單,是否將所有的功能放到一個單元測試中來進行測試,可以自主決定。教程中仍然采用粒度最小化原則進行測試。
測試粒度一:null測試 StudentServiceImplTest.java
```java
/**
* 參數為null測試
*/
@Test(expected = IllegalArgumentException.class)
public void findByIdNullArgument() {
this.studentService.findById(null);
}
```
測試粒度2:調用測試
```java
/**
* 調用測試
*/
@Test
public void findById() {
// 準備調用時的參數及返回值
// 發起調用
// 斷言返回值與預期相同
// 斷言接收到的參數與預期相同
}
```
按注釋補充代碼如下:
```java
/**
* 調用測試
*/
@Test
public void findById() {
// 準備調用時的參數及返回值
Long id = new Random().nextLong();
Student mockReturnStudent = new Student();
Mockito.when(this.studentRepository.findById(id)).thenReturn(Optional.of(mockReturnStudent));
// 發起調用
Student student = this.studentService.findById(id);
// 斷言返回值與預期相同
Assertions.assertThat(student).isEqualTo(mockReturnStudent);
// 斷言接收到的參數與預期相同
ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(this.studentRepository).findById(longArgumentCaptor.capture());
Assertions.assertThat(longArgumentCaptor.getValue()).isEqualTo(id);
}
```
單元測試通過,說明功能符合我們的預期。
### C層
相對于M層,C層由于需要與前臺對接,所以在測試的過程中測試點相對要多一些。
初始化 StudentControllerTest
```java
@Test
public void getById() {
// 準備傳入參數的數據
// 準備服務層替身被調用后的返回數據
// 按接口規范,向url以規定的參數發起get請求。
// 斷言請求返回了正常的狀態碼
// 斷言C層進行了數據轉發(替身接收的參數值符合預期)
// 斷言返回的json數據符合前臺要求
}
```
按注釋分步完成代碼:
```
import org.assertj.core.internal.bytebuddy.utility.RandomString; ?
@Test
public void getById() throws Exception {
// 準備傳入參數的數據
Long id = new Random().nextLong();
// 準備服務層替身被調用后的返回數據
Student student = new Student();
student.setId(id); ?
student.setSno(new RandomString(6).nextString()?); ?
student.setName(new RandomString(8).nextString()); ?
student.setKlass(new Klass()); ?
student.getKlass().setId(new Random().nextLong()); ?
Mockito.when(this.studentService.findById(Mockito.anyLong())).thenReturn(student);
// 按接口規范,向url以規定的參數發起get請求。
// 斷言請求返回了正常的狀態碼
String url = "/Student/" + id.toString() ;
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get(url))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
// 斷言C層進行了數據轉發(替身接收的參數值符合預期)
ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(this.studentService).findById(longArgumentCaptor.capture());
Assertions.assertThat(longArgumentCaptor.getValue()).isEqualTo(id);
// 斷言返回的json數據符合前臺要求
DocumentContext documentContext =JsonPath.parse(mvcResult.getResponse().getContentAsString()); ?
LinkedHashMap studentHashMap = documentContext.json(); ?
Assertions.assertThat(studentHashMap.get("id")).isEqualTo(Integer.valueOf(id.toString()));
Assertions.assertThat(studentHashMap.get("sno")).isEqualTo(student.getSno());
Assertions.assertThat(studentHashMap.get("name")).isEqualTo(student.getName());
LinkedHashMap klassHashMap = (LinkedHashMap) studentHashMap.get("klass");
Assertions.assertThat(klassHashMap.get("id")).isEqualTo(Integer.valueOf(student.getKlass().getId().toString()));
}
```
* ? 前臺獲取某個學生生,需要將這些值展示到V層或供C層使用,所以返回值在準備好這些數據。
* ? 另一種獲取隨機字符串的方法
* ?? 通過兩次轉換,將json字符串轉換為java中的LinkedHashMap對象
接下來,啟動單元測試并按單元測試提示完善功能代碼或修正單元測試代碼:
```
java.lang.AssertionError: Status
Expected :200
Actual :404
<Click to see difference>
```
404錯誤說明請求的地址未找到,該錯誤的產生無非就兩個原因:1. 請求時URL不小心拼寫錯了。 2. 后臺沒有對應建立好請求地址對應的映射。通過檢查發現當前屬于第2個原因。
```
@GetMapping("{id}") ?
public Student getById(Long id) {
return this.studentService.findById(id);
}
```
再次進行測試:
```
Assertions.assertThat(longArgumentCaptor.getValue()).isEqualTo(id);
org.junit.ComparisonFailure:
Expected :555414603L ?
Actual :null
```
* ? 該值是隨機生成的,每次執行單元測試都會生成一個隨機值,因面你本地顯示的值與教程不同是正確的。
該錯誤提示我們:應該是使用傳入的ID值來調用M層,但實際上卻使用了null來調用。這是由于我們在C層未能成功的接收傳入ID值造成的。
```java
public Student getById(@PathParam("id")? Long id) {
return this.studentService.findById(id);
}
```
再次測試仍然是剛剛的錯誤,這仍然說明C層沒有接收到傳入的ID值。最終通過檢查發現原來獲取路徑變量的應該使用`@PathVariable`而非`@PathParam`:
```
public Student getById(@PathVariable? Long id) {
return this.studentService.findById(id);
}
```
再次進行單元測試,測試通過。單元測試看似寫了較多的代碼,但其實開發的效率并不低。在這種開發模式下,我們無需向數據庫中寫入真實的數據(實際上這項工作在一些稍大型的一些有外鍵約束的項目中非常的沉重),也不會額外啟動一個前臺或是類似于postman的工作。更重要的是還為此代碼在后期項目更新的過程中提供了功能保障。長期來看,其不失為一種高效的開發方式。
## JsonPath
在將json字符串變更為java可識別的對象時,使用了`JsonPath.parse`方法。實際上springboot已經內置了`JsonPah`并將其快速的應用到了模擬請求返回值的斷言中。剛剛單元測試中對json數據的斷言還可以改寫成這樣。
```
@Test
public void getById() throws Exception {
// 準備傳入參數的數據
Long id = new Random().nextLong();
// 準備服務層替身被調用后的返回數據
Student student = new Student();
student.setId(id);
student.setSno(new RandomString(6).nextString());
student.setName(new RandomString(8).nextString());
student.setKlass(new Klass());
student.getKlass().setId(new Random().nextLong());
Mockito.when(this.studentService.findById(Mockito.anyLong())).thenReturn(student);
// 按接口規范,向url以規定的參數發起get請求。
// 斷言請求返回了正常的狀態碼
String url = "/Student/" + id.toString() ;
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get(url))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("id").value(id)) ?
.andExpect(MockMvcResultMatchers.jsonPath("sno").value(student.getSno())) ?
.andExpect(MockMvcResultMatchers.jsonPath("name").value(student.getName())) ?
.andExpect(MockMvcResultMatchers.jsonPath("klass.id").value(student.getKlass().getId())) ?
.andExpect(MockMvcResultMatchers.jsonPath("klass.name").value(student.getKlass().getName())) ?
.andReturn();
// 斷言C層進行了數據轉發(替身接收的參數值符合預期)
ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(this.studentService).findById(longArgumentCaptor.capture());
Assertions.assertThat(longArgumentCaptor.getValue()).isEqualTo(id);f
}
```
# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.4](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.4) | - |
| JsonPath | [https://github.com/json-path/JsonPath](https://github.com/json-path/JsonPath)| - |
- 序言
- 第一章: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
- 總結
- 開發規范
- 備用