本节让我们看看如何使用代码的方式来完成点击教师列表的动作,初步的接触下自动化的魅力。 当前测试用例下,我们需要模拟用户点击教师列表,以测试教师列表被点击后,是否在组件的`@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) |