有了公用的多择组件后我们尝试使用它打造班级选择组件。来到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>) {
}
}
```
# 单元测试
我们已经掌握了对嵌套组件的测试的方法,本例中将展示一种更贴近于官方最佳实践的测试组织方法。以HttpClient为例,angular同时提供了可用于生产环境的HttpClientModule以及用于测试环境的HttpClientTestingModule来做为HttpClient的提供者。官方的这种做法使得在测试过程中引入HttpClient的替身变成一件非常轻松的事情。
反观我们当前的测试,将相关测试文件统一加入到TestModule快速的解决了测试过程中依赖问题,这本无可厚非,但却不是一个好的习惯。从简单的意义上来讲,由于并没有贴近于官言的最佳实践所以这种模式必然会存在问题,当前没有发现问题的原因只能是我们对angular理解的还不够深入,应用的还不够广泛;从复杂点的意义上来,在实际的前端开发中团队需要抽离出如用户登录、注销、权限验证、菜单生成、AppOnReady等众多公用服务做为单独的angular库在应用到不同的项目中,而在对应的模块中同步建立测试模块以提供测试替身则符合angular的规范及习惯,使得团队其它项目引入公用服务时的单元测试更加规范以提升整体的开发效率。
## CoreTestingModule
在CoreMoudle中同步建立测试Module ---- CoreTestingModule
```
panjiedeMac-Pro:core panjie$ ng g m CoreTesting
CREATE src/app/core/core-testing/core-testing.module.ts (197 bytes)
```
建立对应的组件替身
```
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相信会有千万种方案,本例给出一种以共生产环境参考:
>[warning] 本示例已超出本教程的解释范围,是生产环境下组织单元测试文件的一种方法,仅做参考。
在CoreTesting模块中声明提供CoreTestingController,以便在单元测试中使用Test.get(CoreTestingController)方法来获取CoreTestingController:
core/core-testing/core-testing.module.ts
```typescript
],
providers: [
CoreTestingController ➊
]
})
export class CoreTestingModule { }
```
* ➊ 声明模块提供CoreTestingController
在测试控制中提供加入、获取相关单元的功能。
src/app/core/core-testing/core-testing-controller.ts
```typescript
/**
* 该方案仅适用于在嵌套组件的数量为1.
* 由于在get方法中直接以instanceof方法获取了相关组件
* 所以如果某个组件在被测试组件中多次被引用时
* 只能获取第一个被push进来的组件
*/
export class CoreTestingController {
/**
* 存储组件、指令或管道
*/
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;
}
}
/**
* 定义一个Clazz类型,用于参数中接收 类、接口等
*/
export type Clazz = new(...args: any[]) => any;
```
组件替身声明与组件具有相同的输入与输出,同时将组件本身添加到测试控制器中。
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 {
/** 数据列表 */
@Input() ➊
list$: Observable<Array<{ name: string }>>;
/** 事件弹射器,用户点选后将最终的结点弹射出去 */
@Output() ➊
changed = new EventEmitter<Array<any>>();
constructor(private coreTestingController: CoreTestingController➋) {
this.coreTestingController.addUnit(this); ➌
}
ngOnInit() {
}
}
```
* ➊ 声明与被替组件具有相同的输入与输出
* ➋ 注入测试控制器
* ➌ 将组件本身加入到测试控制器中
# 小试牛刀
来到course模块的班级多选组件中进行嵌套组件测试如下:
course/klass-multiple-select/klass-multiple-select.component.spec.ts
```typescript
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [KlassMultipleSelectComponent],
imports: [
CoreTestingModule ➊
]
})
.compileComponents();
}));
fit('嵌套组件MultipleSelectComponent测试', () => {
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对应的测试模块CoreTestingModule
* ➋ 像angular官方一样优雅地获取测试控制器
* ➌ 像angular官方一样优雅地获取被嵌套组件
* ➍ 此处的MultipleSelectComponent无论是真实的组件还是与组件同名的替身均可正常工作
# 功能开发
班级多选组件中可供选择的班级来源于数据表klass,为此按MVC的开发理论,首先补充KlassService用于获取全部的班级列表。
## 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方法来获取全部的班级数据。由于在前面的章节中并没有为klass建立单独的service,而是选择在klass模块的index组件中直接向后台发请的请求。所以此时需要去查看对应组件中获取全部班级的代码。最终确认获取全部方法的接口信息为:`GET http://localhost:8080/Klass?name=`,于是获取全部班级的代码如下:
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) {
}
/**
* 获取所有班级
*/
all(): Observable<Klass[]> {
const httpParams = new HttpParams().append('name', '');
return this.httpClient.get<Klass[]>(this.url, {params: httpParams});
}
}
```
### 单元测试
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', () => {
// 数据准备,调用被测方法
const service: KlassService = TestBed.get(KlassService);
let result;
service.all().subscribe((data) => {
result = data;
});
// 断言发起请求符合预期
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);
}
}
```
### 单元测试
该组件依赖于KlassService,为此在进行单元测试前先建立KlassService的测试替身KlassStubService
```
panjiedeMac-Pro:service panjie$ ng g s KlassStub --skip-tests
CREATE src/app/service/klass-stub.service.ts (138 bytes)
```
在替身中同样创建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;
}
}
```
补充班级选择组件ngOnInit方法及changed方法的测试:
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$);
});
```
单元测试通过,本节完成:
![](https://img.kancloud.cn/fe/29/fe296d7a079ea45df6500f79435df3ea_320x136.png)
# 本节小测
请参考本节中的测试示例,请尝试在course文件夹中建立CourseModule对应的测试CourseTestingModule以及相关文件。
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [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
- 第一节: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
- 总结
- 开发规范
- 备用