angularu在单元测试中如何模拟HTTP请求延迟

随着对angular应用学习的深刻,如何在单元测试中模拟http请求延迟便提上了日程。在没有http请求延迟之前,单元测试中咱们都是使用of()来手动发送数据的。of()方法在单元测试中无疑带来了巨大的便利性,但因为同步的机制,使其未能彻底的模拟中在生成环境中http请求延迟可能对组件带来的冲击,因此在启用of()进行单元测试后没法保障该组件在生产环境中是100%可靠运行的。html

阅读本文须要对angular单元测试有必定了解。

sample

简单举个of()的例子来查看其如何成为异步请求单元测试中的不可靠元素。typescript

V层显示学生的姓名npm

<h1>{{student.name}}</h1>

C层初始化学生的值为undefinedjson

/**
    * 该值初始化为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);
   }

控制台打印结果以下:async

1接收到了订阅的数据
2ngOnInit执行完毕,开始渲染V层

对应的程序执行流程以下:
image.pngide

生产环境

而生产环境中因为进行真正的http请求,该请求是异步的且必然有延迟,因此真实的控制台状况以下:函数

2ngOnInit执行完毕,开始渲染V层
1接收到了订阅的数据

就便成了这样:单元测试

image.png

也就是说:在有异步请求的状况下,此单元测试并没能保障该组件在生产环境中的正确运行。学习

接下来,本文将提供两种模拟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异步发送数据的目的,因此在单元测试中执行相应的测试代码执行流程以下:

image.png

如上图,在单元测试中发生了异常。此异常提醒咱们组件在初始化的过程当中,没有对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销毁组件

执行流程以下:

image.png

怎样才能保证在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的做用是模拟时钟的推动,咱们测试其是否能够影响RxJSdelay操做符

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 testinggetById方法改写为:

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,该官方提供的方法很好的解决了上述问题。

本文做者:河北工业大学梦云智开发团队 潘杰
相关文章
相关标签/搜索