有了公用的多擇組件后我們嘗試使用它打造班級(jí)選擇組件。來到course模塊中,并新建KlassMultipleSelect組件。
```
panjiedeMac-Pro:course panjie$ ng g c KlassMultipleSelect
CREATE src/app/course/klass-multiple-select/klass-multiple-select.component.sass (0 bytes)
CREATE src/app/course/klass-multiple-select/klass-multiple-select.component.html (36 bytes)
CREATE src/app/course/klass-multiple-select/klass-multiple-select.component.spec.ts (721 bytes)
CREATE src/app/course/klass-multiple-select/klass-multiple-select.component.ts (328 bytes)
UPDATE src/app/course/course.module.ts (478 bytes)
```
V層初始化如下:
course/klass-multiple-select/klass-multiple-select.component.html
```html
<app-multiple-select [list$]="klasses$" (changed)="onChange($event)"></app-multiple-select>
```
C層初始化如下:
course/klass-multiple-select/klass-multiple-select.component.ts
```typescript
import { Component, OnInit } from '@angular/core';
import {Observable} from 'rxjs';
import {Klass} from '../../norm/entity/Klass';
@Component({
selector: 'app-klass-multiple-select',
templateUrl: './klass-multiple-select.component.html',
styleUrls: ['./klass-multiple-select.component.sass']
})
export class KlassMultipleSelectComponent implements OnInit {
klasses$: Observable<Klass[]>;
constructor() { }
ngOnInit() {
}
onChange($event: Array<Klass>) {
}
}
```
# 單元測(cè)試
我們已經(jīng)掌握了對(duì)嵌套組件的測(cè)試的方法,本例中將展示一種更貼近于官方最佳實(shí)踐的測(cè)試組織方法。以HttpClient為例,angular同時(shí)提供了可用于生產(chǎn)環(huán)境的HttpClientModule以及用于測(cè)試環(huán)境的HttpClientTestingModule來做為HttpClient的提供者。官方的這種做法使得在測(cè)試過程中引入HttpClient的替身變成一件非常輕松的事情。
反觀我們當(dāng)前的測(cè)試,將相關(guān)測(cè)試文件統(tǒng)一加入到TestModule快速的解決了測(cè)試過程中依賴問題,這本無可厚非,但卻不是一個(gè)好的習(xí)慣。從簡單的意義上來講,由于并沒有貼近于官言的最佳實(shí)踐所以這種模式必然會(huì)存在問題,當(dāng)前沒有發(fā)現(xiàn)問題的原因只能是我們對(duì)angular理解的還不夠深入,應(yīng)用的還不夠廣泛;從復(fù)雜點(diǎn)的意義上來,在實(shí)際的前端開發(fā)中團(tuán)隊(duì)需要抽離出如用戶登錄、注銷、權(quán)限驗(yàn)證、菜單生成、AppOnReady等眾多公用服務(wù)做為單獨(dú)的angular庫在應(yīng)用到不同的項(xiàng)目中,而在對(duì)應(yīng)的模塊中同步建立測(cè)試模塊以提供測(cè)試替身則符合angular的規(guī)范及習(xí)慣,使得團(tuán)隊(duì)其它項(xiàng)目引入公用服務(wù)時(shí)的單元測(cè)試更加規(guī)范以提升整體的開發(fā)效率。
## CoreTestingModule
在CoreMoudle中同步建立測(cè)試Module ---- CoreTestingModule
```
panjiedeMac-Pro:core panjie$ ng g m CoreTesting
CREATE src/app/core/core-testing/core-testing.module.ts (197 bytes)
```
建立對(duì)應(yīng)的組件替身
```
panjiedeMac-Pro:core panjie$ cd core-testing/
panjiedeMac-Pro:core-testing panjie$ ng g c MultipleSelect --skip-tests
CREATE src/app/core/core-testing/multiple-select/multiple-select.component.sass (0 bytes)
CREATE src/app/core/core-testing/multiple-select/multiple-select.component.html (30 bytes)
CREATE src/app/core/core-testing/multiple-select/multiple-select.component.ts (305 bytes)
UPDATE src/app/core/core-testing/core-testing.module.ts (307 bytes)
panjiedeMac-Pro:core-testing panjie$
```
參考angular官方的HttpClientTestingModule提供HttpTestingController,提供CoreTestingController
```
panjiedeMac-Pro:core-testing panjie$ ng g class CoreTestingController --skip-tests
CREATE src/app/core/core-testing/core-testing-controller.ts (39 bytes)
```
# 一種示例
如何整理CoreTestingModule以及CoreTestingController相信會(huì)有千萬種方案,本例給出一種以共生產(chǎn)環(huán)境參考:
>[warning] 本示例已超出本教程的解釋范圍,是生產(chǎn)環(huán)境下組織單元測(cè)試文件的一種方法,僅做參考。
在CoreTesting模塊中聲明提供CoreTestingController,以便在單元測(cè)試中使用Test.get(CoreTestingController)方法來獲取CoreTestingController:
core/core-testing/core-testing.module.ts
```typescript
],
providers: [
CoreTestingController ?
]
})
export class CoreTestingModule { }
```
* ? 聲明模塊提供CoreTestingController
在測(cè)試控制中提供加入、獲取相關(guān)單元的功能。
src/app/core/core-testing/core-testing-controller.ts
```typescript
/**
* 該方案僅適用于在嵌套組件的數(shù)量為1.
* 由于在get方法中直接以instanceof方法獲取了相關(guān)組件
* 所以如果某個(gè)組件在被測(cè)試組件中多次被引用時(shí)
* 只能獲取第一個(gè)被push進(jìn)來的組件
*/
export class CoreTestingController {
/**
* 存儲(chǔ)組件、指令或管道
*/
private units = new Array<any>();
constructor() {
}
/**
* 添加單元(組件、指令或管道)
* @param unit 單元
*/
addUnit(unit: any): void {
this.units.push(unit);
}
/**
* 獲取單元(組件、指令或管道)
* @param clazz 類型
*/
get(clazz: Clazz): any {
let result: any = null;
this.units.forEach((value) => {
if (value.constructor.name === clazz.name) {
result = value;
}
});
return result;
}
}
/**
* 定義一個(gè)Clazz類型,用于參數(shù)中接收 類、接口等
*/
export type Clazz = new(...args: any[]) => any;
```
組件替身聲明與組件具有相同的輸入與輸出,同時(shí)將組件本身添加到測(cè)試控制器中。
core/core-testing/multiple-select/multiple-select.component.ts
```typescript
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {CoreTestingController} from '../core-testing-controller';
import {Observable} from 'rxjs';
@Component({
selector: 'app-multiple-select',
templateUrl: './multiple-select.component.html',
styleUrls: ['./multiple-select.component.sass']
})
export class MultipleSelectComponent implements OnInit {
/** 數(shù)據(jù)列表 */
@Input() ?
list$: Observable<Array<{ name: string }>>;
/** 事件彈射器,用戶點(diǎn)選后將最終的結(jié)點(diǎn)彈射出去 */
@Output() ?
changed = new EventEmitter<Array<any>>();
constructor(private coreTestingController: CoreTestingController?) {
this.coreTestingController.addUnit(this); ?
}
ngOnInit() {
}
}
```
* ? 聲明與被替組件具有相同的輸入與輸出
* ? 注入測(cè)試控制器
* ? 將組件本身加入到測(cè)試控制器中
# 小試牛刀
來到course模塊的班級(jí)多選組件中進(jìn)行嵌套組件測(cè)試如下:
course/klass-multiple-select/klass-multiple-select.component.spec.ts
```typescript
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [KlassMultipleSelectComponent],
imports: [
CoreTestingModule ?
]
})
.compileComponents();
}));
fit('嵌套組件MultipleSelectComponent測(cè)試', () => {
const coreTestingController = TestBed.get(CoreTestingController); ?
const multipleSelect = coreTestingController.get(MultipleSelectComponent)? as MultipleSelectComponent; ?
// 斷言input
expect(multipleSelect.list$).toBe(component.klasses$);
// 斷言output
spyOn(component, 'onChange');
const klasses = [new Klass(null, null, null)];
multipleSelect.changed.emit(klasses);
expect(component.onChange).toHaveBeenCalledWith(klasses);
});
```
* ? 引入MultipleSelectComponent所在CoreModule對(duì)應(yīng)的測(cè)試模塊CoreTestingModule
* ? 像angular官方一樣優(yōu)雅地獲取測(cè)試控制器
* ? 像angular官方一樣優(yōu)雅地獲取被嵌套組件
* ? 此處的MultipleSelectComponent無論是真實(shí)的組件還是與組件同名的替身均可正常工作
# 功能開發(fā)
班級(jí)多選組件中可供選擇的班級(jí)來源于數(shù)據(jù)表klass,為此按MVC的開發(fā)理論,首先補(bǔ)充KlassService用于獲取全部的班級(jí)列表。
## service
```javascript
panjiedeMac-Pro:service panjie$ ng g s klass
CREATE src/app/service/klass.service.spec.ts (328 bytes)
CREATE src/app/service/klass.service.ts (134 bytes)
```
增加all方法來獲取全部的班級(jí)數(shù)據(jù)。由于在前面的章節(jié)中并沒有為klass建立單獨(dú)的service,而是選擇在klass模塊的index組件中直接向后臺(tái)發(fā)請(qǐng)的請(qǐng)求。所以此時(shí)需要去查看對(duì)應(yīng)組件中獲取全部班級(jí)的代碼。最終確認(rèn)獲取全部方法的接口信息為:`GET http://localhost:8080/Klass?name=`,于是獲取全部班級(jí)的代碼如下:
service/klass.service.ts
```typescript
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {Klass} from '../norm/entity/Klass';
import {HttpClient, HttpParams} from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class KlassService {
private url = 'http://localhost:8080/Klass';
constructor(private httpClient: HttpClient) {
}
/**
* 獲取所有班級(jí)
*/
all(): Observable<Klass[]> {
const httpParams = new HttpParams().append('name', '');
return this.httpClient.get<Klass[]>(this.url, {params: httpParams});
}
}
```
### 單元測(cè)試
service/klass.service.spec.ts
```typescript
import {TestBed} from '@angular/core/testing';
import {KlassService} from './klass.service';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {Klass} from '../norm/entity/Klass';
describe('KlassService', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
]
}));
it('should be created', () => {
const service: KlassService = TestBed.get(KlassService);
expect(service).toBeTruthy();
});
fit('all', () => {
// 數(shù)據(jù)準(zhǔn)備,調(diào)用被測(cè)方法
const service: KlassService = TestBed.get(KlassService);
let result;
service.all().subscribe((data) => {
result = data;
});
// 斷言發(fā)起請(qǐng)求符合預(yù)期
const testingController: HttpTestingController = TestBed.get(HttpTestingController);
const request = testingController.expectOne((req) => req.url === 'http://localhost:8080/Klass');
expect(request.request.headers.has('name'));
expect(request.request.method).toEqual('GET');
// 斷言成功的接收到返回值
const klasses = [new Klass(null, null, null)];
request.flush(klasses);
expect(result).toBe(klasses);
});
});
```
## C層
course/klass-multiple-select/klass-multiple-select.component.ts
```typescript
import {Component, EventEmitter, OnInit, Output} from '@angular/core';
import {Observable} from 'rxjs';
import {Klass} from '../../norm/entity/Klass';
import {KlassService} from '../../service/klass.service';
@Component({
selector: 'app-klass-multiple-select',
templateUrl: './klass-multiple-select.component.html',
styleUrls: ['./klass-multiple-select.component.sass']
})
export class KlassMultipleSelectComponent implements OnInit {
klasses$: Observable<Klass[]>;
@Output()
changed = new EventEmitter<Klass[]>();
constructor(private klassService: KlassService) {
}
ngOnInit() {
this.klasses$ = this.klassService.all();
}
onChange($event: Array<Klass>) {
this.changed.emit($event);
}
}
```
### 單元測(cè)試
該組件依賴于KlassService,為此在進(jìn)行單元測(cè)試前先建立KlassService的測(cè)試替身KlassStubService
```
panjiedeMac-Pro:service panjie$ ng g s KlassStub --skip-tests
CREATE src/app/service/klass-stub.service.ts (138 bytes)
```
在替身中同樣創(chuàng)建all方法。
service/klass-stub.service.ts
```typescript
import {Observable} from 'rxjs';
import {Klass} from '../norm/entity/Klass';
export class KlassStubService {
constructor() {
}
all(): Observable<Klass[]> {
return null;
}
}
```
補(bǔ)充班級(jí)選擇組件ngOnInit方法及changed方法的測(cè)試:
course/klass-multiple-select/klass-multiple-select.component.spec.ts
```typescript
providers: [
{provide: KlassService, useClass: KlassStubService}
]
fit('onChange', () => {
let result;
component.changed.subscribe((data) => {
result = data;
});
const klasses = [new Klass(null, null, null)];
component.onChange(klasses);
expect(result).toBe(klasses);
});
fit('ngOnInit', () => {
const klassService: KlassService = TestBed.get(KlassService);
const klasses$ = of([new Klass(null, null, null)]);
spyOn(klassService, 'all').and.returnValue(klasses$);
component.ngOnInit();
expect(component.klasses$).toBe(klasses$);
});
```
單元測(cè)試通過,本節(jié)完成:

# 本節(jié)小測(cè)
請(qǐng)參考本節(jié)中的測(cè)試示例,請(qǐng)嘗試在course文件夾中建立CourseModule對(duì)應(yīng)的測(cè)試CourseTestingModule以及相關(guān)文件。
# 參考文檔
| 名稱 | 鏈接 | 預(yù)計(jì)學(xué)習(xí)時(shí)長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.4](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.4) | - |
- 序言
- 第一章: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 前后臺(tái)對(duì)接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三節(jié) CRUD之C增數(shù)據(jù)
- 1 新建組件并映射路由【前】
- 2 模板驅(qū)動(dòng)表單【前】
- 3 httpClient post請(qǐng)求【前】
- 4 保存數(shù)據(jù)【后】
- 5 組件間調(diào)用【前】
- 第四節(jié) CRUD之U改數(shù)據(jù)
- 1 路由參數(shù)【前】
- 2 請(qǐng)求映射【后】
- 3 前后臺(tái)對(duì)接【前】
- 4 更新數(shù)據(jù)【前】
- 5 更新某個(gè)教師【后】
- 6 路由器鏈接【前】
- 7 觀察者模式【前】
- 第五節(jié) CRUD之D刪數(shù)據(jù)
- 1 綁定到用戶輸入事件【前】
- 2 刪除某個(gè)教師【后】
- 第六節(jié) 代碼重構(gòu)
- 1 文件夾化【前】
- 2 優(yōu)化交互體驗(yàn)【前】
- 3 相對(duì)與絕對(duì)地址【前】
- 第三章 班級(jí)管理
- 第一節(jié) JPA初始化數(shù)據(jù)表
- 第二節(jié) 班級(jí)列表
- 1 新建模塊【前】
- 2 初識(shí)單元測(cè)試【前】
- 3 初始化原型【前】
- 4 面向?qū)ο蟆厩啊?/a>
- 5 測(cè)試HTTP請(qǐng)求【前】
- 6 測(cè)試INPUT【前】
- 7 測(cè)試BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后臺(tái)對(duì)接【前】
- 第三節(jié) 新增班級(jí)
- 1 初始化【前】
- 2 響應(yīng)式表單【前】
- 3 測(cè)試POST請(qǐng)求【前】
- 4 JPA插入數(shù)據(jù)【后】
- 5 單元測(cè)試【后】
- 6 惰性加載【前】
- 7 對(duì)接【前】
- 第四節(jié) 編輯班級(jí)
- 1 FormGroup【前】
- 2 x、[x]、{{x}}與(x)【前】
- 3 模擬路由服務(wù)【前】
- 4 測(cè)試間諜spy【前】
- 5 使用JPA更新數(shù)據(jù)【后】
- 6 分層開發(fā)【后】
- 7 前后臺(tái)對(duì)接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五節(jié) 選擇教師組件
- 1 初始化【前】
- 2 動(dòng)態(tài)數(shù)據(jù)綁定【前】
- 3 初識(shí)泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再識(shí)單元測(cè)試【前】
- 7 其它問題
- 第六節(jié) 刪除班級(jí)
- 1 TDD【前】
- 2 TDD【后】
- 3 前后臺(tái)對(duì)接
- 第四章 學(xué)生管理
- 第一節(jié) 引入Bootstrap【前】
- 第二節(jié) NAV導(dǎo)航組件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三節(jié) footer組件【前】
- 第四節(jié) 歡迎界面【前】
- 第五節(jié) 新增學(xué)生
- 1 初始化【前】
- 2 選擇班級(jí)組件【前】
- 3 復(fù)用選擇組件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校驗(yàn)【后】
- 7 唯一性校驗(yàn)【后】
- 8 @PrePersist【后】
- 9 CM層開發(fā)【后】
- 10 集成測(cè)試
- 第六節(jié) 學(xué)生列表
- 1 分頁【后】
- 2 HashMap與LinkedHashMap
- 3 初識(shí)綜合查詢【后】
- 4 綜合查詢進(jìn)階【后】
- 5 小試綜合查詢【后】
- 6 初始化【前】
- 7 M層【前】
- 8 單元測(cè)試與分頁【前】
- 9 單選與多選【前】
- 10 集成測(cè)試
- 第七節(jié) 編輯學(xué)生
- 1 初始化【前】
- 2 嵌套組件測(cè)試【前】
- 3 功能開發(fā)【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成測(cè)試
- 7 @Input 異步傳值【前】
- 8 值傳遞與引入傳遞
- 9 @PreUpdate【后】
- 10 表單驗(yàn)證【前】
- 第八節(jié) 刪除學(xué)生
- 1 CSS選擇器【前】
- 2 confirm【前】
- 3 功能開發(fā)與測(cè)試【后】
- 4 集成測(cè)試
- 5 定制提示框【前】
- 6 引入圖標(biāo)庫【前】
- 第九節(jié) 集成測(cè)試
- 第五章 登錄與注銷
- 第一節(jié):普通登錄
- 1 原型【前】
- 2 功能設(shè)計(jì)【前】
- 3 功能設(shè)計(jì)【后】
- 4 應(yīng)用登錄組件【前】
- 5 注銷【前】
- 6 保留登錄狀態(tài)【前】
- 第二節(jié):你是誰
- 1 過濾器【后】
- 2 令牌機(jī)制【后】
- 3 裝飾器模式【后】
- 4 攔截器【前】
- 5 RxJS操作符【前】
- 6 用戶登錄與注銷【后】
- 7 個(gè)人中心【前】
- 8 攔截器【后】
- 9 集成測(cè)試
- 10 單例模式
- 第六章 課程管理
- 第一節(jié) 新增課程
- 1 初始化【前】
- 2 嵌套組件測(cè)試【前】
- 3 async管道【前】
- 4 優(yōu)雅的測(cè)試【前】
- 5 功能開發(fā)【前】
- 6 實(shí)體監(jiān)聽器【后】
- 7 @ManyToMany【后】
- 8 集成測(cè)試【前】
- 9 異步驗(yàn)證器【前】
- 10 詳解CORS【前】
- 第二節(jié) 課程列表
- 第三節(jié) 果斷
- 1 初始化【前】
- 2 分頁組件【前】
- 2 分頁組件【前】
- 3 綜合查詢【前】
- 4 綜合查詢【后】
- 4 綜合查詢【后】
- 第節(jié) 班級(jí)列表
- 第節(jié) 教師列表
- 第節(jié) 編輯課程
- TODO返回機(jī)制【前】
- 4 彈出框組件【前】
- 5 多路由出口【前】
- 第節(jié) 刪除課程
- 第七章 權(quán)限管理
- 第一節(jié) AOP
- 總結(jié)
- 開發(fā)規(guī)范
- 備用