spring具體完善、強大的單元測試體系。下面,我們使用單元測試來測試班級的保存功能。
首先我們來到controller/KlassController.java,使用alt+enter快捷鍵來快速的生成測試文件:

如我們所見,經過一系列的操作后IDEA為我們在對應的測試文件夾下生成了對應的測試文件`KlassControolerTest.java`。
```java
package com.mengyunzhi.springBootStudy.controller;
import org.junit.Test;
import static org.junit.Assert.*;
public class KlassControllerTest {
@Test ?
public void save() {
}
}
```
* ? 表示save方法為一個測試用例

點擊此文件的任一綠色箭頭,便可啟動單元測試。此單元測試為基于`junit4`(一個在java下的測試工具)的測試,在此基礎上如想進行Spring項目的單元測試,還需要進行以下配置。
## Spring單元測試
將測試類使用`@RunWith(SpringRunner.class)`、`@SpringBootTest`來標明該類為一個spring測試類。
conroller/KlassControllerTest.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; ①
import static org.junit.Assert.*;
@RunWith(SpringRunner.class) ②
@SpringBootTest ②
public class KlassControllerTest {
@Test
public void save() {
}
}
```
## 測試SAVE方法
想測試控制器中的save方法,我們需要一個叫做`MockMvc 模擬MVC`的家伙來幫忙。
controller/KlassControllerTest.java
```
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;
...
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc ?
public class KlassControllerTest {
@Autowired ?
MockMvc mockMvc;
@Test
public void save() {
}
}
```
* ? 在此類中自動配置MockMvc
* ? 自動裝配MockMvc對象
### 模擬POST請求
代碼如下:
```
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
...
@Test
public void save() throws Exception { ?
String url = "/Klass"; ?
MockHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.post(url) ?
.contentType("application/json;charset=UTF-8") ?
.content("{\"name\":\"測試單元測試班級\"}") ?;
this.mockMvc.perform(postRequest)?
.andDo(MockMvcResultHandlers.print()?) ?
.andExpect(MockMvcResultMatchers.status().is(201));?
}
```
* ? 請求地址,注意**不**需要`http://localhost:8080`。
* ? 構建一個指向url的模擬POST請求。
* ? 當前發送數據的格式為json;發送數據的編碼為utf-8(可省略)。
* ? 發送的主體內容為:`{"name":"測試單元測試班級"}`,其中`\"`為`"`的轉義符號,用于在字符串中輸入`"`。
* ? 發送剛剛構建的postRequest請求。
* ? 該請求可能得不到正確的執行(比如URL地址寫錯了),未正常執行時將拋出異常。
* ? 成功發送請求后...
* ? 打印請求的結果到控制臺
* ? 然后進行斷言
* ? 斷言返回的狀態碼為201
> 閱讀一些長的代碼時,要嘗試將其做為課文來讀。至于這些功能是如何具體實現的,在面向對象的世界里,我們并不關心。
與augular的單元測試一樣,spring的單元測試也可以獨立進行,所以在執行單元測試前,我們完全停止項目的運行:

然后執行單元測試:

在測試方法前顯示了綠色的對勾,表明該方式測試通過。
### 測試中加入教師信息
在前面的測試中加入教師信息時,首先在數據庫中了添加一條用于測試的教師信息:

在此時由于后臺整個項目未啟動,所以此時數據庫中并沒有任何數據表(每次系統啟動時創建新表、停止時刪除創建的表),那么當然也就無法向數據表中添加用于測試的教師數據了。其實即使是我們想辦法添加了測試的數據,單元測試在啟動時也會自動去刪除原有的數據表然后創建空的數據的表,所以只要我們的數據表策略是`create-drop`,那么就沒有辦法使用原來的方法添加測試教師數據。
每次單元測試時都清空數據表在單元測試中非常有必要的,這保證了我們每次啟動單元測試時數據的初始環境都是一致的,減少了單元測試在不同環境下的不確定性。那么此時如果想添加測試教師,則需要在代碼中進行添加:
#### TeacherReopository
前面我們建立了操作Klass表的KlassRepository,參考該文件我們建立操作Teacher表的TeacherRepository
repository/TeacherRepository.java
```
package com.mengyunzhi.springBootStudy.repository;
import com.mengyunzhi.springBootStudy.entity.Teacher;
import org.springframework.data.repository.CrudRepository;
/**
* 教師倉庫
*/
public interface TeacherRepository extends CrudRepository<Teacher, Long> {
}
```
#### 自動裝配
接著來到需要使用該倉庫進行數據新增的controller/KlasscontrollerTest.java
```java
import com.mengyunzhi.springBootStudy.repository.TeacherRepository;
...
public class KlassControllerTest {
@Autowired
MockMvc mockMvc;
/*教師倉庫*/
@Autowired
TeacherRepository teacherRepository; ①
@Test
public void save() throws Exception {
```
### 請求主體中加入教師信息
最后,我們在單元測試中利用teacherRepository來新建一個測試教師,最獲取其ID放置到POST請求的主體中。
controller/KlasscontrollerTest.java
```
/*教師倉庫*/
@Autowired
TeacherRepository teacherRepository; ①
@Test
public void save() throws Exception {
Teacher teacher = new Teacher(); ?
teacher.setName("潘杰"); ?
teacher.setEmail("panjie@yunzhiclub.com"); ?
teacher.setUsername("panjie"); ?
teacherRepository.save(teacher); ?
String content = String.format("{\"name\":\"測試單元測試班級\", \"teacher\": {\"id\":%s}}", teacher.getId()); ?
String url = "/Klass";
MockHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.post(url)
.contentType("application/json;charset=UTF-8")
.content(content); ②
this.mockMvc.perform(postRequest)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().is(201));
}
```
* ? 實例化教師
* ? 設置教師的屬性
* ? 將教師保存到數據庫中
* ? 拼接請求主體對應的字符串
測試:
```
MockHttpServletRequest:
HTTP Method = POST
Request URI = /Klass
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8"]
Body = {"name":"測試單元測試班級", "teacher": {"id":1}}
Session Attrs = {}
```
### 添加斷言
我們前面的測試確認了以下內容:
* [ ] 請求的地址是正確的
* [ ] 按接口發送數據,后臺是能夠成功接收的
* [ ] 后臺處理后,返回201的狀態碼
但數據是否真的被保存成功了呢?下面,我們加入數據保存成功的斷言。
```java
import org.junit.Assert;
/*班級*/
@Autowired
KlassRepository klassRepository; ①
/**
* 班級保存測試
* 1. 建立一個供測試的教師
* 2. 拼接請求的JSON串
* 3. 模擬請求并斷言返回了201
* 4. 斷言新增數據成功
* @throws Exception
*/
@Test
public void save() throws Exception {
Teacher teacher = new Teacher();
teacher.setName("潘杰");
teacher.setEmail("panjie@yunzhiclub.com");
teacher.setUsername("panjie");
teacherRepository.save(teacher);
String content = String.format("{\"name\":\"測試單元測試班級\", \"teacher\": {\"id\":%s}}", teacher.getId());
String url = "/Klass";
MockHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.post(url)
.contentType("application/json;charset=UTF-8")
.content(content);
this.mockMvc.perform(postRequest)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().is(201));
List<Klass> klasses = (List<Klass>)? this.klassRepository.findAll(); ?
Assert.assertEquals(klasses.size(), 1); ?
Klass klass = klasses.get(0); ?
Assert.assertEquals(klass.getName(), "測試單元測試班級"); ?
Assert.assertEquals(klass.getTeacher().getName(), "潘杰"); ?
}
```
* ? findAll()查詢出數據表中的全部數據。
* ? 將返回的值轉換為List<Klass>類型。
* ? 斷言有1條班級數據
* ? 獲取該班級數據
* ? 斷言班級的名稱為測試名稱
* ? 斷言班級的中的教師名稱為測試名稱
最行,執行單元測試,測試通過代碼數據添加成功,方法正確。
# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.3.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.3.5) | - |
| Testing the Web Layer | [https://spring.io/guides/gs/testing-web/](https://spring.io/guides/gs/testing-web/) | 15 |
| Spring MVC Test Framework | [https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/testing.html#spring-mvc-test-framework](https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/testing.html#spring-mvc-test-framework) | 30 |
- 序言
- 第一章: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
- 總結
- 開發規范
- 備用