我們在上個小節中剛剛講了`規范`,在本節中開始前我們先為idea安裝一個規范插件。打開idea,使用`ctrl+,`打開參數設置,然后輸入plugins:

在右側的窗口中輸入alibaba

點擊install安裝,完成后重啟idea,此時一個代碼規范約束的插件便被成功的安裝了。以后當我們在書寫一不太規范的代碼時,該插件則會進行自動提示。
# M層 -- 多態
JAVA的多態性允許我們在StudentService中有了一個findAll方法的同時,再寫一個同名findAll方法:
```
/**
* 學生
*/
public interface StudentService {
...
/**
* 查詢分頁信息
*
* @param pageable 分頁條件
* @return 分頁數據
*/
Page<Student> findAll(Pageable pageable);
/**
* 綜合查詢
* @param name containing 姓名
* @param sno beginWith 學號
* @param klassId equal 班級ID
* @param pageable
* @return
*/
Page<Student> findAll(String name, String sno, Long klassId, Pageable pageable); ?
```
* ? 方法名仍然為findAll,但由于參數不同,所以在其它的對象調用StudentService.findAll時,java是可以通過判斷參數的數量、類型來區分我們具體是想調用哪個findAll方法。
> 一個findAll方法,有多種形態,稱為面向對象的多態性
## 實現類
在實現類中添加方法實現:
```
@Service
public class StudentServiceImpl implements StudentService {
...
@Override
public Page<Student> findAll(String name, String sno, Long klassId, Pageable pageable) {
Klass klass = new Klass(); ?
klass.setId(klassId); ?
return this.studentRepository.findAll(name, sno, klass, pageable); ?
}
}
```
* ? 根據klassId構造用Klass
* ? 調用數據倉庫,完成查詢并返回
## 單元測試
找開位于測試文件夾的StudentServiceImplTest并增加測試方法findAllSpecs。該測試的思路和以前的一樣,我們只需要測試findAll(String name, String sno, Long klassId, Pageable pageable)是否將數據成功的轉發給數據倉庫層即可。至于數據倉庫層是否成功的執行了查詢功能,按分層理論這是數據倉庫層需要關心的事情。
```
import java.util.Arrays;
...
@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentServiceImplTest {
private static Logger logger = LoggerFactory.getLogger(StudentServiceImplTest.class);
@MockBean
StudentRepository studentRepository;
...
@Test
public void findAllSpecs() {
/* 參數初始化 */
String name = "hello";
String sno = "032282";
Long klassId = 1L;
Pageable pageable = PageRequest.of(0, 2);
List<Student> students = Arrays.asList();
Page<Student> mockStudentPage = new PageImpl<>(students, pageable, 100L) ?;
/* 設置模擬返回值 */
Mockito.when(this.studentRepository
.findAll(Mockito.eq(name)?,
Mockito.eq(sno),
Mockito.any(Klass.class),
Mockito.eq(pageable)))
.thenReturn(mockStudentPage) ?;
/* 調用測試方法,獲取返回值并斷言與預期相同 */
Page<Student> returnStudentPage = this.studentService.findAll(name, sno, klassId, pageable);
Assertions.assertThat(returnStudentPage).isEqualTo(mockStudentPage); ?
}
}
```
* ? 調用PageImpl<>(當前頁內容, 分頁信息, 總條數)來構建返回值
* ? 當調用findAll的第一個參數與name相等(eq)時
* ? 當參數為符合:`參數1等于(eq)name對象(值:hello),參數2等于(eq)sno對象(值:032282) , 參數1為任意(any)的班級,參數4等于(eq)pageable對象(值:第0頁每頁2條)`規則時,調用findAll方法將模擬返回mockStudentPage對象。
* 預期返回了mockStudentPage對象。
由于預期返回了mockStudentPage對象,則說明在studentService中調用studentRepository.findAll時,傳入的參數符合`參數1等于(eq)name對象(值:hello),參數2等于(eq)sno對象(值:032282) , 參數1為任意(any)的班級,參數4等于(eq)pageable對象(值:第0頁每頁2條)`規則。進而說明調用name, sno, pageable均是我們傳入的值。而是否按我們的預期傳入了klass還需要使用參數捕獲器來獲取:
```
public class StudentServiceImplTest {
...
@Test
public void findAllSpecs() {
...
Assertions.assertThat(returnStudentPage).isEqualTo(mockStudentPage);
/* 獲取M層調用studentRepository的findAll方法時klass的參數值,并進行斷言 */
ArgumentCaptor<Klass> klassArgumentCaptor = ArgumentCaptor.forClass(Klass.class);
Mockito.verify(this.studentRepository).findAll(Mockito.eq?(name), Mockito.eq(sno), klassArgumentCaptor.capture()①, Mockito.eq(pageable));
Assertions.assertThat(klassArgumentCaptor.getValue().getId()).isEqualTo(klassId); ②
}
}
```
* ? Mockito.eq與前面作用相同
* ① 把第三個參數替換為klassArgumentCaptor.capture()來獲取參數的值
* ② 斷言klass中id的值
### 請思索
在剛剛的測試中,我們將測試代碼做以下替換,同樣可以順利通過測試,你知道其中的原因嗎?
```
Mockito.verify(this.studentRepository).findAll(Mockito.any(String.class), Mockito.any(String.class), klassArgumentCaptor.capture(), Mockito.any(Pageable.class));
Assertions.assertThat(klassArgumentCaptor.getValue().getId()).isEqualTo(klassId);
```
## Null值處理
我們在進行方法調用的原則是:如果該參數沒有標記為@NotNull,則表示其是可以接收并順利處理null值的。而如果M層在findAll方法中如果接收的Pageable為null,則會在調用數據倉庫層時發生異常,而這是我們不希望看到了。
```
import javax.validation.constraints.NotNull;
...
public interface StudentService {
...
Page<Student> findAll(String name, String sno, Long klassId, @NotNull Pageable pageable);
}
```
實現類:
```
import org.springframework.util.Assert;
import javax.validation.constraints.NotNull;
...
public class StudentServiceImpl implements StudentService {
...
@Override
public Page<Student> findAll(String name, String sno, Long klassId, @NotNull Pageable pageable) {
Assert.notNull(pageable, "Pageable不能為null");
...
}
}
```
### null值測試
```
public class StudentServiceImplTest {
...
@Test(expected = IllegalArgumentException.class) ①
public void findAllSpecsNullValidate() {
try {
this.studentService.findAll(null, null, null, null);
} catch (Exception e②) {
Assertions.assertThat(e.getMessage()).isEqualTo("Pageable不能為null");
throw e; ③
}
}
}
```
* ① 該測試應該會拋出 IllegalArgumentException
* ② 所有的異常都繼承了Exception (java.lang.Exception屬于java內置的類,使用時無需import),所以只要有異常拋出,并必然被此catch捕獲到
* ③ 將獲取到的異常向上拋出,并被 ① 獲取
# 重寫C層
打開StudentController -> findAll方法,在原代碼的基礎上加入參數name,sno以及klassId:
```
public class StudentController {
...
@GetMapping
public Page<Student> findAll(
@RequestParam String name, ?
@RequestParam String sno, ?
@RequestParam Long klassId, ?
@RequestParam int page,
@RequestParam int size) {
return this.studentService.findAll(PageRequest.of(page, size));
}
```
修改調用方法:
```
public Page<Student> findAll(
@RequestParam String name,
@RequestParam String sno,
@RequestParam Long klassId,
@RequestParam int page,
@RequestParam int size) {
return this.studentService.findAll(
name, ?
sno, ?
klassId, ?
PageRequest.of(page, size));
}
```
## 單元測試
前面我們一直在強調單元測試是對自己代碼功能的保障,此時我們剛剛變動了StudentController的代碼,那么我們校驗下單元測試是否起到了應有的作用。我們打開單元測試文件StudentControllerTest,找到并找行findAll單元測試:
最終得到了以下錯誤提示:
```
java.lang.AssertionError: Status
Expected :200
Actual :400
<Click to see difference>
```
期待是200,最終返回了400。在控制臺中找到錯誤提示行并進行點擊,報錯的行數如上:
```
public class StudentControllerTest {
...
@Test
public void findAll() throws Exception {
...
MvcResult mvcResult = this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("page", "1")
.param("size", "2"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk()) ★
.andReturn();
```
說明返回的狀態碼為400,并不是我們期望的200。而400通常代表參數綁定錯誤,在我們當前的用例中:由于我們在C層的方法中聲明需要接收參數name、sno及klassId,而單元測試在測試時,只傳入了參數page與size。所以在參數的綁定過程中spring不清楚需要用什么值來綁定name、sno、klassId,進而發生了400錯誤。
> 在生產環境中,C層的單元測試報400錯誤是需要引起重視的,因為這意味著我們后臺的請求接口的規范發生了變化,而前臺如果想適應這個變化就必須同步進行修改。
單元測試報錯的原因可以分為兩種:第一種是功能的修正導致歷史的單元測試代碼不能適應新的功能需求;第二種是在增加、修正關聯代碼時對歷史的功能造成了影響。如果情況屬性第一種,則應該按新功能修正單元測試代碼,如果情況屬性第二種,則應該進行近一步的排查。我們此處的單元測試則屬于第一種,解決的方法是修正單元測試代碼:
在原請求的參數的基礎上,我們加入以下三個參數:
```
public class StudentControllerTest {
...
@Test
public void findAll() throws Exception {
...
MvcResult mvcResult = this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("name", "testName") ?
.param("sno", "testSno") ?
.param("klassId", "1") ?
.param("page", "1")
.param("size", "2"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
```
加全請求參數后,重新運行該單元測試,400的錯誤消息,得到了新的錯誤:
```
java.lang.IllegalArgumentException: json string can not be null or empty
at com.jayway.jsonpath.internal.Utils.notEmpty(Utils.java:386)
at com.jayway.jsonpath.internal.ParseContextImpl.parse(ParseContextImpl.java:36)
at com.jayway.jsonpath.JsonPath.parse(JsonPath.java:599)
at com.mengyunzhi.springbootstudy.controller.StudentControllerTest.findAll(StudentControllerTest.java:87) ★
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
```
★處對應的代碼如下:
```
public class StudentControllerTest {
...
@Test
public void findAll() throws Exception {
...
logger.info("將返回值由string轉為json,并斷言接收到了分頁信息");
LinkedHashMap returnJson = JsonPath.parse(mvcResult.getResponse().getContentAsString()).json(); ★
Assertions.assertThat(returnJson.get("totalPages")).isEqualTo(2); // 總頁數
```
該錯誤的出現說明了兩點:1. 未發生400錯誤,說明參數綁定成功,證明剛剛修正的代碼正確。2. 將返回值轉換為JSON時發生錯誤,說明返回不再符合預期,按錯誤提示猜測試返回值為empty或null。
既然對返回數據產生了懷疑,則可以添加斷點并啟用debug模式來對問題進行調試,在錯誤的行上打個斷點:

使用debug模式運行測試

在控制臺中查看返回值mvcResult:

結果顯示返回了空字符串,而不是我們預期的分頁信息了。這是由于C層的代碼在改變了調用M層的方法引起的,我們再來查看C層代面在引入綜合查詢前后的變化
```
public class StudentController {
...
@GetMapping
public Page<Student> findAll(
@RequestParam String name, ? ①
@RequestParam String sno, ? ①
@RequestParam Long klassId, ? ①
@RequestParam int page,
@RequestParam int size) {
return this.studentService.findAll(PageRequest.of(page, size)); ? ②
return this.studentService.findAll(
name,
sno,
klassId,
PageRequest.of(page, size)); ? ②
}
```
* ① 代碼變動、引起了400問題
* ② 代碼變動,引起了返回空值的問題
在前面的測試代碼中,我們測試的邏輯是:C層代碼調用的是`this.studentService.findAll(Pageable)`方法。而修改后的邏輯變更為調用`this.studentService.findAll(Strng, String, Long, Pageable)`方法。此時單元測試的報錯及時的提醒我們C層調用的邏輯發生了變化。如果邏輯是正確的則應該修正單元測試代碼;如果邏輯是錯誤的,則應該修正單元測試代碼。則修正單元測試代碼如下:
```
public class StudentControllerTest {
...
@Test
public void findAll() throws Exception {
...
Mockito.when(this.studentService.findAll(Mockito.any(Pageable.class)))
.thenReturn(mockOutStudentPage); ?
Mockito.when(this.studentService
.findAll(Mockito.anyString()?,
Mockito.anyString(),
Mockito.anyLong()?,
Mockito.any(Pageable.class)))
.thenReturn(mockOutStudentPage); ?
...
```
* ? 請求的參數值類型為String時
* ? 請求的參數值類型為Long時
再次運行單元測試,測試通過。
## 可選參數及空參數
按接口的設計,前臺在與后臺進行交互時,查詢參數name、sno及klassId為optional(可選項),也就是說:我們允許用戶在查詢時不輸入此參數。而剛剛在測試的過程中我們發現:如果不輸入name sno或klassId則會得到一個400錯誤。在spring中,可以為@RequestParam設置`required=false`來解決該問題:
```
public class StudentController {
...
@GetMapping
public Page<Student> findAll(
@RequestParam(required = false?) String name,
@RequestParam(required = false) String sno,
@RequestParam(required = false) Long klassId,
@RequestParam int page,
@RequestParam int size) {
return this.studentService.findAll(
name,
sno,
klassId,
PageRequest.of(page, size));
}
```
* ? 該參數是可選項(非required),當用戶未傳入該參數時,name的值設置為null。
重新運行單元測試通過,表示我們的此處的修改并未對原功能造成影響。
### 單元測試
本著單元測試粒度最小的原則,我們StudentControllerTest中新建findAllRequestParam方法:
```
public class StudentControllerTest {
...
/**
* 請求參數測試
* @throws Exception
*/
@Test
public void findAllRequestParam() throws Exception {
String url = "/Student";
logger.info("只傳入page size,不報錯");
this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("page", "1")
.param("size", "2"))
.andExpect(MockMvcResultMatchers.status().isOk());
logger.info("不傳page報錯");
this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("size", "2"))
.andExpect(MockMvcResultMatchers.status().is(HttpStatus.BAD_REQUEST.value()));
logger.info("不傳size報錯");
this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("page", "1"))
.andExpect(MockMvcResultMatchers.status().is(400));
}
```
# 小測試
在C層的單元測試findAll中我們雖然斷言了`必然調用studentService.findAll方法`,但卻沒有對調用該方法時向個參數的傳入值進行斷言。也就是說如果C層的代碼被不小心寫成:`return this.studentService.findAll(sno, name, klassId, PageRequest.of(page, size))`的話,我們也無從察覺。請參數M層的測試補充該部分,以確保C層的轉發是正確的。
# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.5) | - |
| mockito anyString | [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyString--](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyString--) | 2 |
| mockito anyLong | [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyLong--](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyLong--) | 2 |
| mockito eq | [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#eq-T-](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#eq-T-) | 2 |
- 序言
- 第一章: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
- 總結
- 開發規范
- 備用