我们在上个小节中首次使用单元测试来进行组件的单独开发,本节继续使用单元测试来自动完成C层的交互。
# 新建测试用例
在上一个测试用例的下方,模仿建一个新的测试用例。
klass/index/index.component.spec.ts
```js
it('测试V层的交互操作', () => {
});
```
* 使用`it(string ,function)`来声明一个测试用例。`string` 是对测试用例的描述,`function`来定义该测试用例的功能。
增加了测试用例后,我们在终端中使用`ng test`来执行测试。
![](https://img.kancloud.cn/7c/3c/7c3c585475cef99d256c65a03085863a_784x176.png)
测试结果页显示我们的当前的测试用例没有`预测(断言)`信息。
> 断言:我`断`定`说`,一定会发生什么,如果结果与我的断言不相符则报错。
## 验证双向数据绑定
在班级index组件的V层中,我们使用了这样的代码想达到将C层数据传给(绑定)V层使用的目的:
klass/index/index.component.html
```
<form (ngSubmit)="onQuery()">
<label>名称:<input type="text" name="params.name"/></label> ①
<button>查询</button>
</form>
```
下面我们使用单元测试来验证数据是否被绑定成功了。同时,为了防止测试用例的执行顺序,我们将当前测试用由`it`变更为`fit`,表示:只执行本测试用例。
klass/index/index.component.spec.ts
```
it('测试V层的交互操作', () => { ✘
fit('测试V层的交互操作', () => { ✚
component.params.name = 'test'; ➊
fixture.detectChanges(); ➋
});
```
* ➊ 设置查询参数`name`的值为`test`。
* ➋ 当组件发生变化时,重新渲染组件。
#### 测试
![](https://img.kancloud.cn/81/1b/811b3f0a7e0b533af37bca4e03c90158_352x179.png)
V层中的input表单并没有如我们所期望的一般变为`test`。由此可见,我们的数据绑定并没有成功。相信当我们初始化V层的时候,你已经发现了这个前台绑定的错误,在前台进行数据绑定时,我们应该这样:
```
<label>名称:<input type="text" name="params.name"/></label> ✘
<label>名称:<input type="text" name="name" [(ngModel)]="params.name"/></label> ✚
```
保存文件后得到如下测试提醒:
```
Failed: Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'. ("<form (ngSubmit)="onQuery()">
<label>名称:<input type="text" name="name" [ERROR ->][(ngModel)]="params.name"/></label>
<button>查询</button>
</form>
"): ng:///DynamicTestModule/IndexComponent.html@1:43
```
看到该错误,相信你一定有种似曾相识的感觉,这是因为该错误已经不是首次出现了。错误产生的原因是:`NgModel`指令属于`FormModule`,而我们当前测试未引入`FormModule`,进而报错。其实该错误与前面的`no provider for HttpClient 未找到httpClient的提供者`的错误性质是一样的。在C中引入`HttpClient`则需要引用`HttpClient`所在的模块`HttpModule`,在V层中使用`ngModel`指令,则需要引用`ngModel`指令所在的模块`FormsModule`。修正的方法如下:
klass/index/index.component.spec.ts
```
import {FormsModule} from '@angular/forms';
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [IndexComponent],
imports: [HttpClientTestingModule, FormsModule] ①
})
.compileComponents();
}));
```
此时再查看运行的结果:
![](https://img.kancloud.cn/ba/b9/bab9f478df9ca4c5017d8c59340aeb03_369x163.png)
### 验证V层向C层绑定数据
刚刚我们验证了C层向V层的数据绑定,接下来继续验证V层向C层的数据绑定。
我们将上个用例的`fit`修改为`it`,然后新建一个`fit`测试用例方法。
klass/index/index.component.spec.ts
```
fit('测试V层向C层绑定', () => {
fixture.whenStable().then(() => { ➊
});
});
```
* ➊ 由于V层的渲染需要的一定的时间(很短,肉眼感受不到),所以在进行V层的一些操作时,我们使用`whenStable().then(function)`来等待V层渲染完毕。
在Angular测试中,我们使用如下方法设置`input`的值:
klass/index/index.component.spec.ts
```
import {By} from '@angular/platform-browser'; ①
fit('测试V层向C层绑定', () => {
fixture.whenStable().then(() => {
const debugElement = fixture.debugElement; ➊
const nameInputElement = debugElement.query(By.css('input[name="name"]')); ➋
const nameInput: HTMLInputElement = nameInputElement.nativeElement; ➌
nameInput.value = 'test1'; ➍
nameInput.dispatchEvent(new Event('input')); ➎
console.log(component.params.name); ➏
});
});
```
* ➊ 获取**所有**供测试的V层元素。
* ➋ 通过CSS选择器来查询出具体的V层的元素。
* ➌ 将获取到的元素转换为`HTMLInputElement`。
* ➍ 设置预输入表单的值。
* ➎ Angular 不知道我们设置了这个 <input> 元素的 value 属性。 在通过调用 dispatchEvent() 触发了该输入框的 input 事件之前,它不能读到那个值。
* ➏ 查看是否成功的绑定到C层。
> 只所以要经过➊➋➌三步操作是由于:Angular 依赖于`[DebugElement](https://www.angular.cn/api/core/DebugElement)`这个抽象层,就可以安全的横跨*其支持的所有平台*。 Angular 不再创建 HTML 元素树,而是创建`[DebugElement](https://www.angular.cn/api/core/DebugElement)`树,其中包裹着相应运行平台上的*原生元素*。`nativeElement`属性会解开`[DebugElement](https://www.angular.cn/api/core/DebugElement)`,并返回平台相关的元素对象。简单的原因就是:由于angular要安全的支持所有平台。
测试结果:
```
Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 0 of 6 SUCCESS (0 secs / 0 secs)
ERROR: 'Spec 'IndexComponent 测试V层向C层绑定' has no expectations.'
Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 0 of 6 SUCCESS (0 secs / 0 secs)
LOG: 'test1'
Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 1 of 6 SUCCESS (0 secs / 0.114 secs)
Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 1 of 6 (skipped 5) SUCCESS (0.137 secs / 0.114 secs)
TOTAL: 1 SUCCESS
TOTAL: 1 SUCCESS
```
![](https://img.kancloud.cn/e9/ad/e9adcb8a1438fbab3b63cfda9aed7d75_952x676.png)
## 断言
我们刚刚在控制台中确认了数据被成功的绑定。但这种确认的方法并不可靠,随着项目推进、需求变更、团队人员增加等因素,我们需要越来越多的主观判断来保证历史的功能仍在正常运行。试想下,我们当前的项目有了3个测试用例,如果人为的来确认这3个测试用例是正确的,则需要逐个将`it`方法变更为`fit`方法,然后观察界面或控制台来确认组件的正确性。在团队开发中,往往是由于成员A变更了自己的功能而导致成员B的功能出现错误。也就是说如果想保证项目的质量,那么成员A在变更了自己的任意代码后都应该把项目中所有的单元测试跑一遍,用眼睛来确认一便来确保自己的变更没有对其它的模块产生影响。这是不现实的。
单元测试的断言很好的解决了此问题,每个预期的结果我们都写在单元测试的代码中,当其它的成员进行增量开发或是修改时,如果不小心影响了我们的功能,那么此时单元测试会自动告知他。这样就使得在大型的团队合作开发的项目中,不会因个别代码的变动而影响其它模块的功能了。
`断言` = `断定说`,还可以理解为`我预测`、`我期望..`等。下面,我们来逐步增加断言进而完成前面的单元测试。
klass/index/index.component.spec.ts
```
fit('测试V层向C层绑定', () => {
expect(component).toBeTruthy(); ① ✚
fixture.whenStable().then(() => {
const debugElement: DebugElement = fixture.debugElement;
const nameInputElement = debugElement.query(By.css('input[name="name"]'));
const nameInput: HTMLInputElement = nameInputElement.nativeElement;
nameInput.value = 'test1';
nameInput.dispatchEvent(new Event('input'));
console.log(component.params.name); ✘
expect(component.params.name).toBe('test1'); ➊
```
* ① 防止报没有任何期望的错误
* ➊ 期望`component.params.name`的值为`test1`
如果我们将`expect(component.params.name).toBe('test1');`变更为`expect(component.params.name).toBe('test2');`,则会得到如下错误:··
```
Expected 'test1' to be 'test2'.
```
它是在说:我们期望的是`test2`,但是得到的是`test1`,这与我们规定的期望不致。有了这个期望后,当其它的成员或几天后的自己不小心犯错后单元测试便会自动告知我们了。
### 修正其它的断言
将此测试用例方法由`fit`变更为`it`后,我们为其它的两个测试用例增加期望。
```
it('should create', () => {
expect(component).toBeTruthy();
const req = httpTestingController.expectOne('http://localhost:8080/Klass?name=');
const klasses = [
new Klass(1, '计科1901班', new Teacher(1, 'zhagnsan', '张三')),
new Klass(2, '软件1902班', new Teacher(2, 'lisi', '李四'))
];
req.flush(klasses);
fixture.detectChanges();
fixture.whenStable().then(() => {
const debugElement: DebugElement = fixture.debugElement;
const tableElement = debugElement.query(By.css('table')); ➊
const nameInput: HTMLTableElement = tableElement.nativeElement; ➋
expect(nameInput.rows.length).toBe(3); ➌
expect(nameInput.rows.item(1).cells.item(1).innerText).toBe('计科1901班'); ➍
expect(nameInput.rows.item(1).cells.item(2).innerText).toBe('张三'); ➎
});
});
it('测试V层的交互操作', () => {
component.params.name = 'test';
fixture.detectChanges();
fixture.whenStable().then(() => {
const debugElement: DebugElement = fixture.debugElement;
const nameInputElement = debugElement.query(By.css('input[name="name"]'));
const nameInput: HTMLInputElement = nameInputElement.nativeElement;
expect(nameInput.value).toBe('test');
});
});
```
* ➊➋ 查找table元素。
* ➌ 断言table中有3行信息:表头1行+数据2行。
* ➍ 断言第1行第1列的值为`计科1901班`。
* ➎ 断言第1行第2列的值为`张三`。
> 在实际的开发中,我们可能不会对数据绑定这种较常规的点进行测试。此小节对双向数据绑定进行了测试,目的在抛砖引玉为以后较复杂的组件测试做基础积累。
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.2.6](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.2.6) | - |
| 组件DOM测试 | [https://www.angular.cn/guide/testing#component-dom-testing](https://www.angular.cn/guide/testing#component-dom-testing) | 15 |
| CSS选择器 | [https://www.w3school.com.cn/cssref/css\_selectors.asp](https://www.w3school.com.cn/cssref/css_selectors.asp) | 10 |
| DebugElement | [https://www.angular.cn/api/core/DebugElement](https://www.angular.cn/api/core/DebugElement) | 5 |
| HTMLTableElement | [https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLTableElement](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLTableElement) | 5 |
|HTMLInputElement | [https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLInputElement](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLInputElement) | 5 |
| 使用`[dispatchEvent()](https://www.angular.cn/)`修改输入值 | [https://www.angular.cn/guide/testing#change-an-input-value-with-dispatchevent](https://www.angular.cn/guide/testing#change-an-input-value-with-dispatchevent) | 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
- 总结
- 开发规范
- 备用