本节让我们看看如何使用代码的方式来完成点击教师列表的动作,初步的接触下自动化的魅力。
当前测试用例下,我们需要模拟用户点击教师列表,以测试教师列表被点击后,是否在组件的`@Output`上接收到了相应的值。比如我们模拟点击教师列表中的最后一个教师:
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
@@ -41,5 +41,10 @@ describe('KlassSelectComponent', () => {
fixture.autoDetectChanges();
component.beChange
.subscribe((data: number) => console.log('接收到了弹出的数据', data));
+ // 模拟点击教师列表中的第二个教师
});
});
```
### 模拟点击option
在响应式表中,模拟点击某个`option`仅需要以下两步:
1. 获取预点击`option`对应的值
2. 将值使用`setValue()`方法送入`FormControl`
```typescript
@@ -44,6 +42,7 @@ describe('KlassSelectComponent', () => {
component.beChange
.subscribe((data: number) => console.log('接收到了弹出的数据', data));
// 模拟点击教师列表中的第二个教师
+ const teacher = component.teachers[1];
+ console.log(teacher);
});
});
```
打开控制台我们得到了一个undefined:
![image-20210325141537067](https://img.kancloud.cn/72/84/7284e1dc5db0f7e5159c3bb1aea068b8_938x156.png)
产生该问题的原因从根上来讲是由于JavaScript的异步机制。从当前应用层面上来讲,是由于我们的MockApi模拟了真实网络的延迟。当前对组件的测试大概发生了以下事件:
![image-20210325142503271](https://img.kancloud.cn/7e/dd/7eddccb418d91aa14ae0ede1de08482d_1306x560.png)
如上图示,由于`MockApi`的延迟发送数据,所以在执行到`const teacher = component.teachers[1];`时,`component.teachers`仍然是个空数组。
```typescript
// 模拟点击教师列表中的第二个教师
+ console.log(component.teachers.length);
const teacher = component.teachers[1];
console.log(teacher);
```
![image-20210325143408038](https://img.kancloud.cn/e6/fb/e6fbfd58b428da84911bc0e6e85bad1c_652x92.png)
但当前我们希望的是单元测试按以下流程执行:
![image-20210325142808204](https://img.kancloud.cn/69/5f/695ff41f6f60e0e8f5c1c77417a4aae2_1350x424.png)
## getTestScheduler()
预使在发生http请求时`MockApi`马上发送数据,则需要使用`jasmine-marbles`提供的`getTestScheduler()`,该方法的作用是:马上发送(本计划延迟发送的)数据 。由于`jasmine-marbles`只被应用在测试环境下,所以我们使用`npm install --save-dev jasmine-marbles`来安装:
```bash
panjie@panjies-Mac-Pro first-app % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app
panjie@panjies-Mac-Pro first-app % npm install --save-dev jasmine-marbles
...
+ jasmine-marbles@0.8.1
added 1 package, removed 1 package and audited 1472 packages in 9.079s
...
```
<hr>
**以下内容了解即可**
实际上`MockApi`提供了两种延迟发送数据的方案:`MockApiInterceptor`与`MockApiTestingInterceptor`。`MockApiInterceptor`用于没有后台的演示环境,该延迟是通过RxJS的`delay`方法实现的;而`MockApiTestingInterceptor`专门用于测试环境,该延迟是通的RxJS的弹珠来实现的。而弹珠测试的数据发送,是可以通过`jasmine-marbles`手动控制的。
**以下内容了解即可**
<hr>
然后我们在当前动态测试模块下引入支持`jasmine-marbles`马上发送数据的`MockApiTestingInterceptor`来替换原`MockApiInterceptor`:
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
-import {MockApiInterceptor} from '@yunzhi/ng-mock-api';
+import {MockApiTestingInterceptor} from '@yunzhi/ng-mock-api/testing';
@@ -22,7 +22,7 @@ describe('KlassSelectComponent', () => {
providers: [
{
provide: HTTP_INTERCEPTORS, multi: true,
- useClass: MockApiInterceptor.forRoot([
+ useClass: MockApiTestingInterceptor.forRoot([
TeacherMockApi
])
}
```
接下来便可以在测试代码中调用`getTestScheduler()`方法来手动控制数据发送了:
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
@@ -5,6 +5,7 @@ import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {TeacherMockApi} from '../../mock-api/teacher.mock.api';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MockApiTestingInterceptor} from '@yunzhi/ng-mock-api/testing';
+import {getTestScheduler} from 'jasmine-marbles';
describe('KlassSelectComponent', () => {
let component: KlassSelectComponent;
@@ -41,6 +42,10 @@ describe('KlassSelectComponent', () => {
fixture.autoDetectChanges();
component.beChange
.subscribe((data: number) => console.log('接收到了弹出的数据', data));
+
+ // 手动控制MockApi发送数据
+ getTestScheduler().flush();
+
// 模拟点击教师列表中的第二个教师
console.log(component.teachers.length);
```
![image-20210325144142916](https://img.kancloud.cn/5b/df/5bdf521b16898577ad7421e40dc7f263_610x82.png)
需要**注意**的是:
1. 这种数据发送方式产生的数据变化并不会被`fixture`感知到,所以如果我们需要查看V层的效果,则需要在数据发送完成后,手动调用`detectChanges()`以使V层重新进行渲染。
2. 引入`MockApiTestingInterceptor`后,在测试中仅当调用`getTestScheduler().flush();`时才会发送数据,否则不会发送数据。
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
@@ -45,6 +45,7 @@ describe('KlassSelectComponent', () => {
// 手动控制MockApi发送数据
getTestScheduler().flush();
+ fixture.detectChanges();
// 模拟点击教师列表中的第二个教师
console.log(component.teachers.length);
```
此时我们将在V层中将查看到教师列表中的两个教师。
### setValue()
最后调用`FormControl`的`setValue()`完成`option`的模拟点击:
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
@@ -51,5 +51,7 @@ describe('KlassSelectComponent', () => {
console.log(component.teachers.length);
const teacher = component.teachers[1];
console.log(teacher);
+
+ component.teacherId.setValue(teacher.id);
});
});
panjie
```
![image-20210325144004611](https://img.kancloud.cn/fd/d0/fdd0f75c2b6ac3dcf1b0978225a4ade9_920x154.png)
## 其它方式
我们还可以在单元测试中使用代码的形式来模拟option的点击(实际上Angular在select中的option点击做的并不好),但此方法使用的代码较多,难度相对较大,实际在生产中我们也很少使用。在这我们仅给出相应的代码供参考:
```typescript
fit('模拟点击option', () => {
expect(component).toBeTruthy();
component.beChange
.subscribe((data: number) => console.log('接收到了弹出的数据', data));
// 手动控制MockApi发送数据
getTestScheduler().flush();
fixture.detectChanges();
// 获取select
const htmlSelect = fixture.debugElement.query(By.css('select')).nativeElement as HTMLSelectElement;
htmlSelect.click();
// 获取第二个选项
const htmlOption = htmlSelect.options[1];
// 用该选项的值设置select的值,从而实现模块点击
htmlSelect.value = htmlOption.value;
// 发送change事件,通知浏览器、angular、观察者select值已经发生了变化
htmlSelect.dispatchEvent(new Event('change'));
});
```
## 上结
本结我们使用`getTestScheduler()`方法控制了`MockApi`返回值的时机。这使得我们在脱离后台的开发中,即可以决定http请求返回什么值,又可以决定其值在什么时候返回。这有利于我们掌握组件在http请求前后的状态。
我们还调用了`formControl`的`setValue()`方法来快速的模拟用户的点击,这为以后使用代码来完全代替不可靠的人工点击打下了基础。
| 名称 | 链接 |
| -------- | ------------------------------------------------------------ |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.2.3.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.2.3.zip) |
- 序言
- 第一章 Hello World
- 1.1 环境安装
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教师管理
- 2.1 教师列表
- 2.1.1 初始化原型
- 2.1.2 组件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 请求后台数据
- 2.2.1 HttpClient
- 2.2.2 请求数据
- 2.2.3 模块与依赖注入
- 2.2.4 异步与回调函数
- 2.2.5 集成测试
- 2.2.6 本章小节
- 2.3 新增教师
- 2.3.1 组件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 对接后台
- 2.3.4 路由
- 2.4 编辑教师
- 2.4.1 组件初始化
- 2.4.2 获取路由参数
- 2.4.3 插值与模板表达式
- 2.4.4 初识泛型
- 2.4.5 更新教师
- 2.4.6 测试中的路由
- 2.5 删除教师
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome图标库
- 2.6.3 firefox
- 2.7 总结
- 第三章 用户登录
- 3.1 初识单元测试
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 着陆组件
- 3.5 @Output
- 3.6 TypeScript 类
- 3.7 浏览器缓存
- 3.8 总结
- 第四章 个人中心
- 4.1 原型
- 4.2 管道
- 4.3 对接后台
- 4.4 x-auth-token认证
- 4.5 拦截器
- 4.6 小结
- 第五章 系统菜单
- 5.1 延迟及测试
- 5.2 手动创建组件
- 5.3 隐藏测试信息
- 5.4 规划路由
- 5.5 定义菜单
- 5.6 注销
- 5.7 小结
- 第六章 班级管理
- 6.1 新增班级
- 6.1.1 组件初始化
- 6.1.2 MockApi 新建班级
- 6.1.3 ApiInterceptor
- 6.1.4 数据验证
- 6.1.5 教师选择列表
- 6.1.6 MockApi 教师列表
- 6.1.7 代码重构
- 6.1.8 小结
- 6.2 教师列表组件
- 6.2.1 初始化
- 6.2.2 响应式表单
- 6.2.3 getTestScheduler()
- 6.2.4 应用组件
- 6.2.5 小结
- 6.3 班级列表
- 6.3.1 原型设计
- 6.3.2 初始化分页
- 6.3.3 MockApi
- 6.3.4 静态分页
- 6.3.5 动态分页
- 6.3.6 @Input()
- 6.4 编辑班级
- 6.4.1 测试模块
- 6.4.2 响应式表单验证
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定义FormControl
- 6.4.6 代码重构
- 6.4.7 小结
- 6.5 删除班级
- 6.6 集成测试
- 6.6.1 惰性加载
- 6.6.2 API拦截器
- 6.6.3 路由与跳转
- 6.6.4 ngStyle
- 6.7 初识Service
- 6.7.1 catchError
- 6.7.2 单例服务
- 6.7.3 单元测试
- 6.8 小结
- 第七章 学生管理
- 7.1 班级列表组件
- 7.2 新增学生
- 7.2.1 exports
- 7.2.2 自定义验证器
- 7.2.3 异步验证器
- 7.2.4 再识DI
- 7.2.5 属性型指令
- 7.2.6 完成功能
- 7.2.7 小结
- 7.3 单元测试进阶
- 7.4 学生列表
- 7.4.1 JSON对象与对象
- 7.4.2 单元测试
- 7.4.3 分页模块
- 7.4.4 子组件测试
- 7.4.5 重构分页
- 7.5 删除学生
- 7.5.1 第三方dialog
- 7.5.2 批量删除
- 7.5.3 面向对象
- 7.6 集成测试
- 7.7 编辑学生
- 7.7.1 初始化
- 7.7.2 自定义provider
- 7.7.3 更新学生
- 7.7.4 集成测试
- 7.7.5 可订阅的路由参数
- 7.7.6 小结
- 7.8 总结
- 第八章 其它
- 8.1 打包构建
- 8.2 发布部署
- 第九章 总结