正式動手寫代碼前先畫一個時序圖,來理清調(diào)用動象、調(diào)用方法名、參數(shù)類型以及返回值幾個重要的因素。

有了時序圖在編碼時就清晰了很多,這與寫報告基本類似:先寫目錄,再補充內(nèi)容。
# 初始化
按時序圖的反方向我們進行代碼初始化
## M層
接口:service/StudentService.java
```
package com.mengyunzhi.springBootStudy.service;
import com.mengyunzhi.springBootStudy.entity.Student;
/**
* 學(xué)生
*/
public interface StudentService {
/**
* 保存
* @param student 保存前的學(xué)生
* @return 保存后的學(xué)生
*/
Student save(Student student);
}
```
實現(xiàn)類:service/StudentServiceImpl.java
```
package com.mengyunzhi.springBootStudy.service;
import com.mengyunzhi.springBootStudy.entity.Student;
import org.springframework.stereotype.Service;
@Service
public class StudentServiceImpl implements StudentService {
@Override
public Student save(Student student) {
return null;
}
}
```
## C層
在controller包中新建StudentController.java控制器
```
package com.mengyunzhi.springBootStudy.controller;
import com.mengyunzhi.springBootStudy.entity.Student;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 學(xué)生控制器
*/
@RestController
@RequestMapping("Student")
public class StudentController {
public Student save() {
return null;
}
}
```
## 總結(jié)
代碼在初始化時,我們可以相對隨意一些,把一些自己想到的寫上即可。不必要求必須有功能,甚至于寫錯了都沒有關(guān)系。因為按TDD的開發(fā)理論,有了初始化的代碼后,我們下一步便是寫測試用例,最后依照測試用例來完成功能代碼的開發(fā)。
# 功能
按TDD的理論,我們分別對C層、M層進行測試開發(fā)。
## C層
TDD = Test-driven development 測試驅(qū)動開發(fā)。開發(fā)步驟大體為:① 初始化 ② 單元測試代碼 ③ 功能代碼。
### 單元測試
首先我們使用idea自動生成測試文件,并初始化如下:
controller/StudentControllerTest.java
```
package com.mengyunzhi.springBootStudy.controller;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
public class StudentControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void save() {
}
}
```
接下來結(jié)合接口規(guī)范分步完成C層的單元測試。接口定義如下:
```
POST /Student
```
#### 參數(shù) Parameters
| type | name | Description | Schema |
| --- | --- | --- | --- |
| **Body** | **學(xué)生** <br> *requried* | 學(xué)生信息 | Student |
#### 返回值 Responses
| HTTP Code | Description | Schema |
| --- | --- | --- |
| **201** | Created | 學(xué)生信息 |
##### 班級信息
| name | type | description |
| --- | --- | --- |
| name <br> *requried?* | string(2-20)? | 學(xué)生名稱 |
| sno <br> *requried unique?* | string(6) | 學(xué)號 |
| klass <br> *requried* | {id: Long} | 班級 |
無論測試什么方法,測試的思路都離不開**輸入**、**計算**與**輸出**。C層的測試也同樣如此:
### 輸入
在C層中,輸入分別對應(yīng)了**請求方法**、**請求地址**與**傳入?yún)?shù)**,我們依次對其進行測試。
```
@Test
public void save() throws Exception {
String url = "/Student"; ①
JSONObject studentJsonObject = new JSONObject(); ②
JSONObject klassJsonObject = new JSONObject(); ③
studentJsonObject.put("sno", "學(xué)號測試"); ④
studentJsonObject.put("name", "姓名測試"); ④
klassJsonObject.put("id", -1); ⑤
studentJsonObject.put("klass", klassJsonObject); ⑥
MvcResult mvcResult = this.mockMvc.perform(
MockMvcRequestBuilders.post(url)⑦
.content(studentJsonObject.toString())
.contentType(MediaType.APPLICATION_JSON_UTF8)
).andExpect(MockMvcResultMatchers.status().is(201))
.andReturn();
}
```
* ① 請求地址
* ② 新建學(xué)生json對象,該對象可以使用toString()方法方便的轉(zhuǎn)為json字符串
* ③ 新建班級json對象
* ④ 設(shè)置學(xué)生實體屬性的值
* ⑤ 設(shè)置班級ID
* ⑥ 將班級json對象關(guān)聯(lián)至學(xué)生json對象上
* ⑦ 發(fā)起POST請求
下面,我們啟動單元測試并結(jié)合單元測試的錯誤提示來修正相應(yīng)的功能代碼。
#### 404
```
java.lang.AssertionError: Response status
Expected :201
Actual :404
```
錯誤404說明使用POST方法請求的Klass路徑?jīng)]有找到,我們來到C層代碼,修正如下:
```
@PostMapping ★
public Student save() {
return null;
}
```
再測試
#### 200
```
java.lang.AssertionError: Response status
Expected :201
Actual :200
```
期望返回201,卻返回了200,說明我們忘記定義返回的狀態(tài)碼了。
```
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Student save() {
return null;
}
```
到此,我們完成輸入中的請求地址、請求方法以及返回狀態(tài)碼的測試。下面結(jié)合**計算**測試來對C層中獲取的值是否符合預(yù)期進行測試。
### 數(shù)據(jù)轉(zhuǎn)發(fā)測試
C層的在數(shù)據(jù)層面的作用為:接收數(shù)據(jù)、校驗數(shù)據(jù)以及數(shù)據(jù)轉(zhuǎn)發(fā)。在此我們分別對接收數(shù)據(jù)及數(shù)據(jù)轉(zhuǎn)發(fā)進行測試(C層的校驗數(shù)據(jù)后面添加)。我們無法直接對C層的數(shù)據(jù)進行測試,在此需要依賴一個Mock的M層來協(xié)助測試數(shù)據(jù)接收與轉(zhuǎn)發(fā)是否成功。
#### 功能代碼
首次接觸這樣的測試用了減小學(xué)習(xí)的難度,我們先把C層中核心的代碼完成:
controller/StudentController.java
```
@RestController
@RequestMapping("Student")
public class StudentController {
@Autowired
StudentService studentService; ①
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Student save(Student student②) {
return studentService.save(student); ③
}
}
```
* ① 自動裝配
* ② 設(shè)置接收參數(shù)及參數(shù)的類型
* ③ 調(diào)用服務(wù)層的相關(guān)方法
而我們測試的重點是:
* [ ] 在③中調(diào)用save方法時傳入的student變量,是否與我們前臺傳入的值相對應(yīng)
* [ ] 調(diào)用③后的返回值是否成功的被前臺接收,如果成功接收,那么接收的值是否正確。
下面,我們圍繞上述兩個測試重點展開測試。
#### Mockito.when
要完成前面的測試任務(wù)則需要解決以下兩個問題:
* 當(dāng)C層調(diào)用studentStervice.save方法時,我們必須能獲取該方法中傳入的值。
* 我們必須能指定studentStervice.save的返回值。
在Mock中我們?nèi)缦轮付ǚ祷刂?contorller/StudentControllerTest.java
```
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static Logger logger = LoggerFactory.getLogger(StudentControllerTest.class); ①
...
@MockBean
private StudentService studentService;
...
@Test
public void save() throws Exception {
...
logger.info("準(zhǔn)備服務(wù)層替身被調(diào)用后的返回數(shù)據(jù)");
Student returnStudent = new Student(); ②
Mockito.when( ?
studentService.save(
Mockito.any(Student.class?)))
.thenReturn(returnStudent?);
...
}
```
* ① 啟用日志
* ② 初始化返回值
* ? 當(dāng)調(diào)用studentService.save方法
* ? 并且接收的參數(shù)的值的類型為Student時
* ? 返回returnStudent
#### ArgumentCaptor<T>
而獲取輸入?yún)?shù)的值,則需要借助于ArgumentCaptor<T>,該類需要設(shè)置一個泛型,表示:你指定什么類型,我就能獲取什么類型的變量值。
contorller/StudentControllerTest.java
```
logger.info("新建參數(shù)捕獲器");
ArgumentCaptor<Student> studentArgumentCaptor = ArgumentCaptor.forClass(Student.class); ?
Mockito.verify(studentService).save(studentArgumentCaptor.capture()); ?
Student passedStudent = studentArgumentCaptor.getValue();
```
* ? 初始化一個可以捕獲Student類型變量的捕獲器
* ? 當(dāng)調(diào)用studentService.save方法時,使用studentArgumentCaptor.capture()來捕獲參數(shù)的值
* ? 獲取捕獲的值
### 完整測試代碼
最終代碼如下:
```
@Test
public void save() throws Exception {
logger.info("準(zhǔn)備輸入數(shù)據(jù)");
String url = "/Student";
JSONObject studentJsonObject = new JSONObject();
JSONObject klassJsonObject = new JSONObject();
studentJsonObject.put("sno", "學(xué)號測試");
studentJsonObject.put("name", "姓名測試");
klassJsonObject.put("id", -1);
studentJsonObject.put("klass", klassJsonObject);
logger.info("準(zhǔn)備服務(wù)層替身被調(diào)用后的返回數(shù)據(jù)");
Student returnStudent = new Student();
Mockito.when(
studentService.save(
Mockito.any(Student.class)))
.thenReturn(returnStudent);
logger.info("發(fā)起請求");
MvcResult mvcResult = this.mockMvc.perform(
MockMvcRequestBuilders.post(url)
.content(studentJsonObject.toString())
.contentType(MediaType.APPLICATION_JSON_UTF8)
).andExpect(MockMvcResultMatchers.status().is(201))
.andReturn();
logger.info("新建參數(shù)捕獲器");
ArgumentCaptor<Student> studentArgumentCaptor = ArgumentCaptor.forClass(Student.class);
Mockito.verify(studentService).save(studentArgumentCaptor.capture());
Student passedStudent = studentArgumentCaptor.getValue();
}
```
#### 輸入斷言
接下來,我們來使用**斷言**確保C層的代碼是正確的:
```
...
Mockito.verify(studentService).save(studentArgumentCaptor.capture());
Student passedStudent = studentArgumentCaptor.getValue();
logger.info("斷言捕獲的對與我們前面?zhèn)魅氲闹档南嗤?quot;);
Assertions.assertThat(passedStudent.getSno()).isEqualTo("學(xué)號測試"); ①
Assertions.assertThat(passedStudent.getName()).isEqualTo("姓名測試"); ②
Assertions.assertThat(passedStudent.getId()).isNull(); ③
Assertions.assertThat(passedStudent.getKlass().getId()).isEqualTo(-1L); ④
}
```
* ① 斷言學(xué)號與POST請求值相同
* ② 斷言姓名與POST請求值相同
* ③ 斷言未接收到ID
* ④ 斷言班級ID與POST請求值相同
最后我們運行測試,并根據(jù)測試來補充C層代碼,最終達到測試通過的目的。
```
org.junit.ComparisonFailure:
Expected :"學(xué)號測試"
Actual :null
```
單元測試提醒我們,接收到的學(xué)號的值為null,我們回到C層來檢查此錯誤產(chǎn)生的原因。通過檢查我們發(fā)現(xiàn)原來在C層的參數(shù)中,我們忘記使用@RequestBody注解了。
controller/StudentController.java
```
public Student save(@RequestBody? Student student) {
return studentService.save(student);
}
```
加入該注解后我們繼續(xù)測試:

測試通過說明我們在C層中成功的接收了POST請求的值。
#### 輸出斷言
為了更好的測試輸出,我們需要在輸出的對象上定義一些特定的數(shù)據(jù):
controller/StudentControllerTest.java
```
logger.info("準(zhǔn)備服務(wù)層替身被調(diào)用后的返回數(shù)據(jù)");
Student returnStudent = new Student();
returnStudent.setId(1L); ?
returnStudent.setSno("測試返回學(xué)號"); ?
returnStudent.setName("測試返回姓名"); ?
returnStudent.setKlass(new Klass()); ?
returnStudent.getKlass().setId(1L); ?
Mockito.when(
studentService.save(
Mockito.any(Student.class)))
.thenReturn(returnStudent);
```
然后我們在斷言前先在控制臺上打印下這個返回值:
```
).andExpect(MockMvcResultMatchers.status().is(201))
.andDo(MockMvcResultHandlers.print()) ?
.andReturn();
```
啟動單元測試我們看看都打印了什么:

其中body字段,即是我們需要的返回值
```
Body = {"id":1,"name":"測試返回姓名","sno":"測試返回學(xué)號","klass":{"id":1,"teacher":null,"name":null}}
```
用肉眼觀察的確是返回了我們規(guī)定好的返回值 ,但這并不可靠,下面我們用代碼來獲取這個返回值,并進行適當(dāng)?shù)臄嘌浴?
```
logger.info("斷言捕獲的對與我們前面?zhèn)魅氲闹档南嗤?quot;);
...
logger.info("獲取返回的值");
String stringReturn = mvcResult.getResponse().getContentAsString(); ?
DocumentContext documentContext = JsonPath.parse(stringReturn); ?
LinkedHashMap studentHashMap = documentContext.json(); ?
Assertions.assertThat(studentHashMap.get("id")).isEqualTo(1); ①?
Assertions.assertThat(studentHashMap.get("sno")).isEqualTo("測試返回學(xué)號"); ①
Assertions.assertThat(studentHashMap.get("name")).isEqualTo("測試返回姓名"); ①
LinkedHashMap klassHashMap = (LinkedHashMap)? studentHashMap.get("klass");
Assertions.assertThat(klassHashMap.get("id")).isEqualTo(1); ①
```
* ? 獲取body字段(返回值)的字符串值
* ? 轉(zhuǎn)換為DocumentContext文檔上下文

* ? 以LinkedHashMap(用鏈表的形式存儲鍵、值對的數(shù)據(jù)結(jié)構(gòu))

* ? 此注用`1`而不是`1L`
* ① 斷言返回的值即是我們前面設(shè)置過的值
* ? 進行強制轉(zhuǎn)換(如果studentHashMap.get("klass")不符合LinkedHashMap,則會報錯)
> 將字符串轉(zhuǎn)換為對象的方法很多,教程的方法是基于spring自帶的JosnPath完成的,這不是最簡單的方式也不是最終我們將應(yīng)用的形式,但做為學(xué)習(xí)的過渡階段,還是需要對其進行簡單的了解。
單元測試通過:

此時,如果我們在C層中忘記定義返回值,或是返回的值并非調(diào)用studentService.save方法而獲取的,則會得到異常錯誤。
### 對接M層測試
在本例中,M層的功能僅僅是將數(shù)據(jù)轉(zhuǎn)發(fā)給數(shù)據(jù)倉庫層,所以其功能及測試代碼均較簡單.
service/StudentServiceImpl.java
```
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
StudentRepository studentRepository;
@Override
public Student save(Student student) {
this.studentRepository.save(student);
return student;
}
}
```
service/StudentServiceImplTest.java
```
...
@MockBean
StudentRepository studentRepository; ①
@Autowired
StudentService studentService; ②
...
@Test
public void save()
Student passStudent = new Student(); ③
Student mockReturnStudent = new Student(); ③
Mockito.when(studentRepository.save(Mockito.any(Student.class)))
.thenReturn(mockReturnStudent); ④
Student returnStudent = this.studentService.save(passStudent); ⑤
ArgumentCaptor<Student> studentArgumentCaptor = ArgumentCaptor.forClass(Student.class); ⑥
Mockito.verify(studentRepository).save(studentArgumentCaptor.capture()); ⑦
Assertions.assertThat(studentArgumentCaptor.getValue()).isEqualTo(passStudent); ⑧
Assertions.assertThat(returnStudent).isEqualTo(mockReturnStudent); ⑨
}
```
* ① MOCK調(diào)用方法
* ② 注入測試服務(wù)
* ③ 初始化傳入值,模擬返回值
* ④ 設(shè)置返回值
* ⑤ 調(diào)用被測試方法
* ⑥ 定義參數(shù)捕獲器
* ⑦ 斷言調(diào)用了studentRepository的save方法,并捕獲其調(diào)用過程中傳入的參數(shù)
* ⑧ 斷言我們傳入studentService值即是studentService傳入studentRepository的值
* ⑨ 斷言studentRepository返回studentService的值,即是studentService返回給我們的值
# 總結(jié)
在整個開發(fā)過程中,單元測試伴隨其中。在生產(chǎn)環(huán)境中也是這樣,編寫單元測試代碼的工作量也會比編寫功能代碼的工作量要高的多。保守來講我們測試10行功能代碼,大概需要20行測試代碼的支持。初步接觸單元測試可能會有抵觸的心理,這個可能理解,筆者在進行一些自用小項目的開發(fā)時,也會時不時拋開單元測試。但如果我們面臨的是團隊開發(fā)、面臨的是大項目開發(fā),單元測試便顯得非常有必要了。有了單元測試,我們在重構(gòu)自己的代碼時,再也不需要畏首畏尾了;有了單元測試,我們再也不怕小白加入團隊與我們共同開發(fā)了;有了單元測試,我們補西檣的時候,再也不怕會不小心拆到東檣了;有了單元測試,我們在BUG修正的時候,再也不用遇到修好1個修壞10個的情況了。
最后,讓我們找到Test文件夾并點擊右鍵,然后選擇Run 'All Tests'來運行整個項目的所有單元測試,以確認我們剛剛的開發(fā)未對歷史的功能造成影響。

測試結(jié)果:

結(jié)果顯示共運行了14個單元測試,但失敗了1個,失敗的為StudentcontrollerTest.save方法,我們左側(cè)列表中的方法并查看報錯內(nèi)容及報錯的位置:
```
java.lang.AssertionError:
Expected :0
Actual :1
<Click to see difference>
...
at com.mengyunzhi.springBootStudy.controller.KlassControllerTest.save(KlassControllerTest.java:93)
...
```
出錯的原因是由于我們在測試3.6.2小節(jié)的時候,將KlassService由原來真實的服務(wù)變更為MockBean引起的。由于在調(diào)用模擬的KlassService的save方法時,并沒有執(zhí)行真正的數(shù)據(jù)新增操作(這是正確的),所以當(dāng)我們使用this.klassRepository進行findAll查找時仍然還是找到0條記錄。g下面,我們按照正確的思路,結(jié)合MockBean來修正原來的save測試。
controller/KlassControllerTest.java
```
@Test
public void save() throws Exception {
...
this.mockMvc.perform(postRequest)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().is(201));
ArgumentCaptor<Klass> klassArgumentCaptor = ArgumentCaptor.forClass(Klass.class);
Mockito.verify(klassService).save(klassArgumentCaptor.capture());
Klass passKlass = klassArgumentCaptor.getValue();
Assertions.assertThat(passKlass.getName()).isEqualTo("測試單元測試班級");
Assertions.assertThat(passKlass.getTeacher().getId()).isEqualTo(teacher.getId());
}
```
修正該方法后,單元測試全部通過,我們便可以認為當(dāng)前的變更未對任何歷史代碼產(chǎn)生影響 ,所以可以放心的提交代碼了。
> 在團隊開發(fā)中,如果你不想其它成員不小心修改了你的代碼或是影響了你負責(zé)代碼的功能,那么請使用嚴(yán)謹?shù)膯卧獪y試吧。
# 參考文檔
| 名稱 | 鏈接 | 預(yù)計學(xué)習(xí)時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.5.9](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.5.9) | \- |
| Mockito | | |
| [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html) | \- | |
- 序言
- 第一章:Hello World
- 第一節(jié):Angular準(zhǔn)備工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二節(jié):Hello Angular
- 第三節(jié):Spring Boot準(zhǔn)備工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四節(jié):Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven國內(nèi)源配置
- 4 package與import
- 第五節(jié):Hello Spring Boot + Angular
- 1 依賴注入【前】
- 2 HttpClient獲取數(shù)據(jù)【前】
- 3 數(shù)據(jù)綁定【前】
- 4 回調(diào)函數(shù)【選學(xué)】
- 第二章 教師管理
- 第一節(jié) 數(shù)據(jù)庫初始化
- 第二節(jié) CRUD之R查數(shù)據(jù)
- 1 原型初始化【前】
- 2 連接數(shù)據(jù)庫【后】
- 3 使用JDBC讀取數(shù)據(jù)【后】
- 4 前后臺對接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三節(jié) CRUD之C增數(shù)據(jù)
- 1 新建組件并映射路由【前】
- 2 模板驅(qū)動表單【前】
- 3 httpClient post請求【前】
- 4 保存數(shù)據(jù)【后】
- 5 組件間調(diào)用【前】
- 第四節(jié) CRUD之U改數(shù)據(jù)
- 1 路由參數(shù)【前】
- 2 請求映射【后】
- 3 前后臺對接【前】
- 4 更新數(shù)據(jù)【前】
- 5 更新某個教師【后】
- 6 路由器鏈接【前】
- 7 觀察者模式【前】
- 第五節(jié) CRUD之D刪數(shù)據(jù)
- 1 綁定到用戶輸入事件【前】
- 2 刪除某個教師【后】
- 第六節(jié) 代碼重構(gòu)
- 1 文件夾化【前】
- 2 優(yōu)化交互體驗【前】
- 3 相對與絕對地址【前】
- 第三章 班級管理
- 第一節(jié) JPA初始化數(shù)據(jù)表
- 第二節(jié) 班級列表
- 1 新建模塊【前】
- 2 初識單元測試【前】
- 3 初始化原型【前】
- 4 面向?qū)ο蟆厩啊?/a>
- 5 測試HTTP請求【前】
- 6 測試INPUT【前】
- 7 測試BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后臺對接【前】
- 第三節(jié) 新增班級
- 1 初始化【前】
- 2 響應(yīng)式表單【前】
- 3 測試POST請求【前】
- 4 JPA插入數(shù)據(jù)【后】
- 5 單元測試【后】
- 6 惰性加載【前】
- 7 對接【前】
- 第四節(jié) 編輯班級
- 1 FormGroup【前】
- 2 x、[x]、{{x}}與(x)【前】
- 3 模擬路由服務(wù)【前】
- 4 測試間諜spy【前】
- 5 使用JPA更新數(shù)據(jù)【后】
- 6 分層開發(fā)【后】
- 7 前后臺對接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五節(jié) 選擇教師組件
- 1 初始化【前】
- 2 動態(tài)數(shù)據(jù)綁定【前】
- 3 初識泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再識單元測試【前】
- 7 其它問題
- 第六節(jié) 刪除班級
- 1 TDD【前】
- 2 TDD【后】
- 3 前后臺對接
- 第四章 學(xué)生管理
- 第一節(jié) 引入Bootstrap【前】
- 第二節(jié) NAV導(dǎo)航組件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三節(jié) footer組件【前】
- 第四節(jié) 歡迎界面【前】
- 第五節(jié) 新增學(xué)生
- 1 初始化【前】
- 2 選擇班級組件【前】
- 3 復(fù)用選擇組件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校驗【后】
- 7 唯一性校驗【后】
- 8 @PrePersist【后】
- 9 CM層開發(fā)【后】
- 10 集成測試
- 第六節(jié) 學(xué)生列表
- 1 分頁【后】
- 2 HashMap與LinkedHashMap
- 3 初識綜合查詢【后】
- 4 綜合查詢進階【后】
- 5 小試綜合查詢【后】
- 6 初始化【前】
- 7 M層【前】
- 8 單元測試與分頁【前】
- 9 單選與多選【前】
- 10 集成測試
- 第七節(jié) 編輯學(xué)生
- 1 初始化【前】
- 2 嵌套組件測試【前】
- 3 功能開發(fā)【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成測試
- 7 @Input 異步傳值【前】
- 8 值傳遞與引入傳遞
- 9 @PreUpdate【后】
- 10 表單驗證【前】
- 第八節(jié) 刪除學(xué)生
- 1 CSS選擇器【前】
- 2 confirm【前】
- 3 功能開發(fā)與測試【后】
- 4 集成測試
- 5 定制提示框【前】
- 6 引入圖標(biāo)庫【前】
- 第九節(jié) 集成測試
- 第五章 登錄與注銷
- 第一節(jié):普通登錄
- 1 原型【前】
- 2 功能設(shè)計【前】
- 3 功能設(shè)計【后】
- 4 應(yīng)用登錄組件【前】
- 5 注銷【前】
- 6 保留登錄狀態(tài)【前】
- 第二節(jié):你是誰
- 1 過濾器【后】
- 2 令牌機制【后】
- 3 裝飾器模式【后】
- 4 攔截器【前】
- 5 RxJS操作符【前】
- 6 用戶登錄與注銷【后】
- 7 個人中心【前】
- 8 攔截器【后】
- 9 集成測試
- 10 單例模式
- 第六章 課程管理
- 第一節(jié) 新增課程
- 1 初始化【前】
- 2 嵌套組件測試【前】
- 3 async管道【前】
- 4 優(yōu)雅的測試【前】
- 5 功能開發(fā)【前】
- 6 實體監(jiān)聽器【后】
- 7 @ManyToMany【后】
- 8 集成測試【前】
- 9 異步驗證器【前】
- 10 詳解CORS【前】
- 第二節(jié) 課程列表
- 第三節(jié) 果斷
- 1 初始化【前】
- 2 分頁組件【前】
- 2 分頁組件【前】
- 3 綜合查詢【前】
- 4 綜合查詢【后】
- 4 綜合查詢【后】
- 第節(jié) 班級列表
- 第節(jié) 教師列表
- 第節(jié) 編輯課程
- TODO返回機制【前】
- 4 彈出框組件【前】
- 5 多路由出口【前】
- 第節(jié) 刪除課程
- 第七章 權(quán)限管理
- 第一節(jié) AOP
- 總結(jié)
- 開發(fā)規(guī)范
- 備用