随着对angular应用学习的深入,如何在单元测试中模拟http请求延迟便提上了日程。在没有http请求延迟以前,单元测试中我们都是使用of()
来手动发送数据的。of()
方法在单元测试中无疑带来了巨大的便利性,但由于同步
的机制,使其未能完全的模拟中在生成环境中http请求延迟可能对组件带来的冲击,所以在启用of()
进行单元测试后无法保障该组件在生产环境中是100%可靠运行的。
阅读本文需要对angular单元测试有一定了解。
sample
简单举个of()
的例子来查看其如何成为异步请求单元测试中的不可靠元素。
V层显示学生的姓名
<h1>{{student.name}}</h1>
C层初始化学生的值为undefined
/**
* 该值初始化为undefined
*/
student: {name: string};
ngOnInit() {
// 调用服务来获取ID为1的学生
this.studentService.getById(1)
.subscribe(student => {
console.log('1接收到了订阅的数据');
this.student = student;
})
console.log('2ngOnInit执行完毕,开始渲染V层');
用于单元测试的测试桩StudentStubService
getById(id: number): Observable<{name: string}> {
const student = {name: 'hebut yunzhi'};
// 使用of()方法来返回可观察者,该观察者在被订阅时将同步发送数据
return of(student);
}
控制台打印结果如下:
1接收到了订阅的数据
2ngOnInit执行完毕,开始渲染V层
对应的程序执行流程如下:
生产环境
而生产环境中由于进行真正的http请求,该请求是异步的且必然有延迟,所以真实的控制台情况如下:
2ngOnInit执行完毕,开始渲染V层
1接收到了订阅的数据
就便成了这样:
也就是说:在有异步请求的情况下,此单元测试并没能保障该组件在生产环境中的正确运行。
接下来,本文将提供两种模拟http请求延迟的方法。
没有service的DEMO:[https://stackblitz.com/edit/a...](https://stackblitz.com/edit/a...
delay操作符
RxJS
提供了delay
操作符来延迟异步发送数据,所以模拟http请求延迟的最简单的方法便是在of()
方法的基础上加入delay
操作符。比如我们将前面的测试桩修正为:
getById(id: number): Observable<{name: string}> {
const student = {name: 'hebut yunzhi'};
// 延迟500MS异步发送数据
return of(student).delay(500);
}
此时便起到了延迟500MS异步发送数据的目的,所以在单元测试中执行相应的测试代码执行流程如下:
如上图,在单元测试中发生了异常。此异常提醒我们组件在初始化的过程中,没有对student进行正确的初始化。为了防止生产环境中发生异常的错误,特对组件修正如下:
/**
* 初始化学生,以防止在组件初始化过程中V层渲染发生undefined异常
*/
student = {} as {name: string};
ngOnInit() {
this.studentService.getById(1)
.subscribe(student => {
// 生产环境中,以下代码将在一定延迟后被异步执行被执行。
this.student = student;
})
问题
以上代码保证了组件初始化的过程未发生异常。
但由于delay
的异步执行机制,当delay
方法在500ms返回数据时,单元测试的方法已经执行完毕了且组件已经由内存释放了。也就是说:虽然返回了数据,但由于接收数据的组件已经不存在了,所以该数据并不能体现在被测试组件的视图中。
简单来讲就是:我们无法在单元测试中来查看、断言studentService.getById
的返回值是符合预期并期能够支持组件正常工作的。
ngOnInit() {
// 调用服务来获取ID为1的学生
this.studentService.getById(1)
.subscribe(student => {
console.log('1接收到了订阅的数据');
this.student = student;
})
console.log('2ngOnInit执行完毕,开始渲染V层');
it('should create', () => {
console.log('3断言组件初始化成功');
expect(component).toBeTruthy();
});
afterEach(() => {
console.log('4销毁组件');
fixture.destroy();
});
执行结果:
2ngOnInit执行完毕,开始渲染V层
3断言组件初始化成功
4销毁组件
执行流程如下:
怎样才能保证在delay操作符500ms后发送数据时,组件并未销毁而且可以正常接收student并用接收到的学生渲染V层呢?
tick()模拟推进时钟
在angular单元测试中为我们提供了tick()
方法来模拟时钟的推进。该方法需要配合fakeAsync
使用,比如:
it('tick test', fakeAsync(() => {
let a = 1;
// 500ms后,将a的值变为2
setTimeout(() => {
a = 2; // ➊
}, 500);
// 断言a的值未发生变化,值为1
expect(a).toEqual(1);
// 使用tick模拟将时钟推进500ms,➊的代码被执行。
tick(500);
// 断言a的值发生变化,值为2
expect(a).toEqual(2);
}));
既然tick
的作用是模拟时钟的推进,我们测试其是否可以影响RxJS
的delay
操作符
it('should create', fakeAsync(() => {
expect(component).toBeTruthy();
// 断言由于delay操作符的原因,commpont.student的值仍然初始化的值:null
expect(component.student).toBeNull();
// 模拟将时钟推进500ms
tick(500);
// 如果tick对rxjs的delay操作符起作用,那么以下断言通过。
// 如果不起作用,那么以下断言执行失败。
expect(component.student).toBeTruthy();
}));
最终的实验结果是以上代码无法通过单元测试,即:tick函数并不对delay操作符起作用。
这本质上是由于RxJS在进行一些延迟
处理的时候,并没有使用js内置的setTimeout等方法,而tick方法进行的模拟时钟推进又仅能在setTimeout等方法上生效,所以:tick方法并不能够影响RxJS的在时间
上的处理进程。
RxJS 调度器补丁
RxJS应该是专门有一个自己的时间调度器(scheduler),该调度器作用于一系列与时间相关的操作符上。所以如果想在单元测试中模拟RxJS的时钟推进,则需要在提供了个假的调度器
来替换原有的真调度器
。官方把这个操作称为patch
--打补丁,具体的方案为在对应的单元测试文件中import zone.js/dist/zone-patch-rxjs-fake-async
。该文件的作用便是替换RxJS中原有的scheduler以达到可以模拟进行时钟推进的目的。
该方法可行,但打补丁
并不正统,有兴趣的可参考官方文档尝试。
弹珠测试
优秀伟大的RxJS为我们提供了RxJS marble testing(弹珠测试)
以有效的在单元测试中手动控制数据的弹出。
使用marble testing
将getById
方法改写为:
marbles可能并未包含在angular的默认package.json中,如果是这样的话,需要手动install: npm install jasmine-marbles
getById(id: number): Observable<{name: string}> {
const student = {name: 'hebut yunzhi'};
// 弹珠测试:等待3个时钟周期(-)后发送数据x,x的值为student。然后发送完成发送(|)
return code('---x|', {x: student});
}
对应单元测试方法修改为:
it('should create', fakeAsync(() => {
expect(component).toBeTruthy();
// 断言由于delay操作符的原因,commpont.student的值仍然初始化的值:null
expect(component.student).toBeNull();
// RxJS弹珠测试发送数据
getTestScheduler().flush();
// 断言student发生了变更
expect(component.student).toBeTruthy();
expect(component.student.name).toEqual('hebut yunzhi');
}));
如上所示,在单元测试中调用了getTestScheduler().flush();
来完成了弹珠测试。如此以来,上述代码高度模拟了http异步请求,高度的与生产环境相一致。单元测试有效的保障了生产环境整个项目的健壮性。
it('should create', () =>
// 如下断言保障了组件在初始化的过程中未发生异常
expect(component).toBeTruthy();
// 模拟生产环境后台异步返回数据
getTestScheduler().flush();
// 保障接收模拟数据后,组件重新渲染未发生异常
fixture.detectChanges();
});
总结
在实际的生产项目中,有一组件在单元测试完全OK的情况下却在线上报了undefined错误。追踪其原因时发现是由of
方法的同步返回数据引起了。为了更好的贴近于生产项目,在单元测试中如何引用异步测试便摆在了眼前。
由于RxJS对时间处理采用了调度器的机制,所以原对setTimeout等方法起作用的tick方法并不能推进RxJS的计时器,从而使得在单元测试中使用RxJS异步测试组件的健壮性。
使用RxJS进行单元测试的正确方法为使用marble testing
,该官方提供的方法很好的解决了上述问题。
本文作者:河北工业大学梦云智开发团队 潘杰
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。