在繼續操作以前,先修正下學生實體。由于我們前面的誤操作,在進行學生實體實始化時,忘記加入班級信息了。下面,我們來共同修正一下學生實體。
norm/entity/student.ts
```
@NgModule({})
export class Student {
id: number;
klass: Klass; ?
name: string;
sno: string;
constructor(data?: { id?: number; klass?: Klass ?; name?: string; sno?: string }) {
if (!data) {
return;
}
this.id = data.id ? data.id : null;
this.klass = data.klass ? data.klass : null; ?
this.name = data.name ? data.name : '';
this.sno = data.sno ? data.sno : '';
}
}
```
* ? 加入了班級信息,幸運的是由于我們使用了更加優化的構造函數。我們剛剛的操作對歷史代碼沒有造成任何影響。
# MVC
在上個小節中,大家使用常規的方法完成新增學生組件的功能,現在我們使用MVC的思想重新開發一遍。
我們剛剛接觸了直接與用戶進行交互的V層 -- add.component.html,以及與V層進行直接交互的C層 -- add.component.ts。按MVC的開發思想:V層負責響應用戶;C層負責接收數據接收、數據較驗、數據轉發;M層則負責邏輯處理。回想下3.6.2刪除班級的小節,我們在后臺進行班級刪除時也正是這么做的。
在使用Angular進行開發時,我們也應該將邏輯處理由C層中剝離出來,進而提升代碼的可讀性、降低軟件維護的成本。當前我們需要一個學生服務來完成學生新增的邏輯功能。
寫代碼之前,我們先簡單畫個圖,這樣自己編寫或是與團隊成員交流的時候會更清晰:

* 方法前面的序號代碼執行順序
* ? 方法名
* ? 輸入參數
* ? 返回值
# M層初始化
與后臺的開發思路一致:我們在app目錄下新建service子目錄 -> 在該目錄下使用angular-cli生成student服務。
```
panjiedeMac-Pro:app panjie$ mkdir service
panjiedeMac-Pro:app panjie$ cd service/
panjiedeMac-Pro:service panjie$ ng g s student
CREATE src/app/service/student.service.spec.ts (338 bytes)
CREATE src/app/service/student.service.ts (136 bytes)
```
自動生成的代碼如下所示:
```
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' ?
})
export class StudentService {
constructor() { }
}
```
? 聲明被注入范圍為`root`,即整個系統。此時我們可以在整個項目像注入這個服務,比如我們需要在student/add/add.component.ts中注入這個服務,則可以直接在該文件中這么寫:
```
constructor(private studentService: StudentService) {
}
```
## 增加SAVE方法
在MVC的思想,我們將原來在C層中進行的請求后臺的操作轉移到StudentService的save方法中。
service/student.service.ts
```
@Injectable({
providedIn: 'root'
})
export class StudentService {
constructor(private httpClient: HttpClient) {
}
/**
* 保存學生
* 直接調用HttpClient post方法
* @param student 學生
* @return 此返回值是個可觀察對象:
* 1. 其它人可以通過 訂閱 操作來獲取該對象后續發送的值。
* 2. 該對象如果發送值,那么該值的類型必然是Student。
*/
save(student: Student): Observable<Student> {
const url = 'http://localhost:8080/Student';
return this.httpClient.post<Student?>(url, student);
}
}
```
我們往往與新增教師、新增班級時沒有返回值不同,在定義新增學生接口時我們定義了其返回值為Student,也就是說后臺需要將持久化后的學生再返回給前臺。this.httpClient.post的功能是發請一個http post請求,其返回值的類型取決于后臺具體返回的類型,也就是說:該方法的返回類型不定,但必然應該有一個類型(哪怕是void),而泛型就恰到好處的可以實現這一點。我們使用?來規定此this.httpClient.post發送的請求的返回值類型為Student。
## 單元測試
和測試組件的方法一致,我們來到service/student.service.spec.ts,并對自動生成的文件進行小幅重構。
```
import {TestBed} from '@angular/core/testing';
import {StudentService} from './student.service';
import {HttpClientTestingModule} from '@angular/common/http/testing';
fdescribe('service -> StudentService', () => {
let service: StudentService; ?
beforeEach(() => TestBed.configureTestingModule({
imports: [HttpClientTestingModule] ①
}));
beforeEach(() => { ?
service = TestBed.get(StudentService); ?
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('save', () => { ?
});
});
```
* ? 將公共的對象(每個測試用例都會用到)向上抽離
* ? 在每個測試用例執行前,本方法內的語句均執行1次
* ? 在每個測試用例前,均重新獲取一個StudentService
* ? 新建測試用例測試save方法
### 完善測試功能
在寫單元測試以前,我們必須要弄清兩個問題:輸入與輸出。在save方法中,輸入的為Teacher,輸出的為一個**可觀察者**。如果想確認這個**可觀察者**發送的數據是否符合我們的預期,則要進行**訂閱**操作。我們按模擬輸入、調用方法、斷言輸出的步驟來編寫以下測試代碼:
```
/**
* 測試新增
* 1. 初始化測試數據
* 2. 調用保存方法并進行訂閱
* 2.1 斷言響應中返回了學生ID信息
* 3. 斷言發起了HTTP POST請
* 4. 斷言請求數據
* 5. 模擬HTTP響應數據
* 6. 斷言訂閱的方法被調用
*/
it('save', () => {
const student: Student = new Student(
{
name: 'test',
klass: new Klass(1, null, null)
});
let called = false;
service.save(student).subscribe①((returnStudent: Student) => {
called = true; ③
expect(returnStudent.id).toBe(-1);
});
const httpTestingController: HttpTestingController = TestBed.get(HttpTestingController);
const req = httpTestingController.expectOne('http://localhost:8080/Student');
expect(req.request.method).toEqual('POST');
const studentBody: Student = req.request.body.valueOf();
expect(studentBody.name).toEqual(student.name);
expect(studentBody.klass.id).toEqual(student.klass.id);
req.flush(new Student({id: -1})); ②
expect(called).toBe(true); ④
});
```
* 程序執行順序 ①②③④

測試通過,說明符合預期,M層開發完畢。
# C層
由于StudentService聲明的被注入范圍為root,所以我們可以在直接在student/add/add.component.ts中注入該服務。
```
export class AddComponent implements OnInit {
student: Student;
formGroup: FormGroup;
constructor(private studentService: StudentService①) {
}
```
* ① 和注入其它協作者的方法一樣。
## 調用M層
然后在需要的方法中直接進行相關調用:
student/add/add.component.ts
```
onSubmit(): void {
this.student = this.formGroup.value;
this.studentService.save(this.student).subscribe★((student: Student) => {①
console.log(student);
});
}
```
* ★ 必須進行訂閱,否則HttpClient將不會發起POST請求。
HttpClient這個被訂閱者有點意思,它像極了現實社會中的房地產商。幾年前的房地產商拿到地以后,下一步就是做模型畫大餅來告知老百姓:我將要蓋一個什么樣的房子,然后價格是多少。如果沒有用戶愿意購買,那么前面就會一直停留計劃之中;只有當用戶真真切切的交了錢,房地產商才會真真正正的去蓋樓。現實社會中這無形的助長了地價的飆升,增加了購房人面臨的延期交房或是不交房的風險。但在計算機的世界時,這卻不失為一種最佳的解決方案:HttpClinet.post方法只是表明將進行一個post請求,而如果沒有人想知道請求結果的話,那我一直不會發起真實請求,只有當有人訂閱了它表明想獲取請求結果時,它才會真真切切的去發起這個HTTP請求。所以如果要保證該請求真實發生,必須對其進行訂閱。
## 單元測試
由于我們在組件中引入了UserService,而在UserService又引入了HttpClient,所以執行原單元測試將會報沒有HttpClient的提供者錯誤。

```
NullInjectorError: StaticInjectorError(DynamicTestModule)\[HttpClient\]: StaticInjectorError(Platform: core)\[HttpClient\]: NullInjectorError: No provider for HttpClient!
```
與解決其它的此類問題的方法相同,我們在單元測試中引入HttpClientTestingModule。
```
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AddComponent],
imports: [
ReactiveFormsModule,
HttpClientTestingModule ①
],
})
.compileComponents();
}));
```
### 增加測試內容
為了確保當用戶添加寫相關的內容后點擊保存按鈕發起我們預期的請求,我們對以前的測試代碼進行以下補充。
```
/**
* 1. 向表單中輸入值
* 2. 點擊保存按鈕
* 3. 斷言輸入的值傳入到了C層
*/
fit('should create', () => {
expect(component).toBeTruthy();
formTest.setInputValue('input[name="name"]', 'testname');
formTest.setInputValue('input[name="sno"]', 'testno');
formTest.clickButton('button[type="submit"]');
fixture.detectChanges();
expect(component.student.name).toEqual('testname');
expect(component.student.sno).toEqual('testno');
savePostTest(); ①
});
/**
* 斷言發起了相關請求
* 斷言在請求的中接收到了對應的值
*/
const savePostTest = (): void => { ②
const httpTestingController: HttpTestingController = TestBed.get(HttpTestingController);
const req = httpTestingController.expectOne('http://localhost:8080/Student');
expect(req.request.method).toEqual('POST');
const student: Student = req.request.body.valueOf();
expect(student.name).toEqual('testname');
expect(student.sno).toEqual('testno');
};
```
由于我們在組件中訂閱返回內容后,僅僅是進行控制臺打印操作,所以未對組件訂閱后的內容進行測試。

測試通過。
# 加入選擇班級組件
最后讓我們加入選擇班級組件
## V層
student/add/add.component.html
```
<label>班級:<app-klass-select (selected)="onSelectKlass($event)"></app-klass-select></label>
```
## C層
student/add/add.component.ts
```
export class AddComponent implements OnInit {
...
klass: Klass; ?
constructor(private studentService: StudentService) {
}
ngOnInit() {
this.student = new Student();
this.formGroup = new FormGroup({
name: new FormControl(''),
sno: new FormControl('')
});
}
onSelectKlass(klass: Klass): void { ?
this.klass = klass;
}
onSubmit(): void {
this.student = this.formGroup.value;
this.student.klass = this.klass; ?
this.studentService.save(this.student).subscribe((student: Student) => {
console.log(student);
});
}
```
## 單元測試
在當前的測試思路下,初始化單元測試時必須先要弄清各個模塊前的依賴關系。

按上述依賴圖,我們需要如下定制測試文件:
```
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AddComponent, KlassSelectComponent①],
imports: [
ReactiveFormsModule,
HttpClientTestingModule,
CoreModule ②
],
})
.compileComponents();
}));
```
* ① 同模塊的該組件加入到declarations中
* ② 不同模塊的將組件所在的模塊加入到imports中
### 增加測試內容
student/add/add.component.spec.ts
```
expect(component).toBeTruthy();
component.klass = new Klass(-1, null, null); ?
formTest.setInputValue('input[name="name"]', 'testname')
...
const savePostTest = (): void => {
...
expect(student.sno).toEqual('testno');
expect(student.klass.id).toEqual(-1); ?
};
```

## 整體測試
最后,將特定方法上的`f`去除,進行整個項目的單元測試.

# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.5.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.5.5) | \- |
- 序言
- 第一章: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
- 總結
- 開發規范
- 備用