正在测试使用setInterval或setTimeout的Angular2组件

Testing Angular2 components that use setInterval or setTimeout

本文关键字:setTimeout Angular2 组件 setInterval 测试      更新时间:2023-09-26

我有一个非常典型、简单的ng2组件,它调用一个服务来获取一些数据(转盘项目)。它还使用setInterval每隔n秒在UI中自动切换转盘幻灯片。它工作得很好,但当运行Jasmine测试时,我得到了错误:"不能在异步测试区域内使用setInterval"。

我尝试将setInterval调用包装在这个.zone.runOutsideAngular(()=>{…})中,但错误仍然存在。我本以为将测试更改为在fakeAsync区域中运行会解决问题,但后来我收到一个错误,说不允许从fakeAsync-测试区域内进行XHR调用(这确实有道理)。

如何在仍然能够测试组件的同时使用服务和间隔进行的XHR调用?我使用的是ng2-rc4,由angular cli生成的项目。非常感谢。

我的组件代码:

constructor(private carouselService: CarouselService) {
}
ngOnInit() {
    this.carouselService.getItems().subscribe(items => { 
        this.items = items; 
    });
    this.interval = setInterval(() => { 
        this.forward();
    }, this.intervalMs);
}

Jasmine规格:

it('should display carousel items', async(() => {
    testComponentBuilder
        .overrideProviders(CarouselComponent, [provide(CarouselService, { useClass: CarouselServiceMock })])
        .createAsync(CarouselComponent).then((fixture: ComponentFixture<CarouselComponent>) => {
            fixture.detectChanges();
            let compiled = fixture.debugElement.nativeElement;
            // some expectations here;
    });
}));

Clean代码是可测试的代码。CCD_ 1有时很难测试,因为时间从来都不是完美的。您应该将setTimeout抽象为一个服务,以便为测试进行模拟。在mock中,您可以使用控件来处理间隔的每个刻度。例如

class IntervalService {
  interval;
  setInterval(time: number, callback: () => void) {
    this.interval = setInterval(callback, time);
  }
  clearInterval() {
    clearInterval(this.interval);
  }
}
class MockIntervalService {
  callback;
  clearInterval = jasmine.createSpy('clearInterval');
  setInterval(time: number, callback: () => void): any {
    this.callback = callback;
    return null;
  }
  tick() {
    this.callback();
  }
}

有了MockIntervalService,您现在可以控制每个刻度,这在测试过程中更容易推理。还有一个间谍可以检查组件被销毁时是否调用了clearInterval方法。

对于您的CarouselService,由于它也是异步的,请参阅这篇文章以获得一个好的解决方案。

以下是使用前面提到的服务的完整示例(使用RC 6)。

import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TestBed } from '@angular/core/testing';
class IntervalService {
  interval;
  setInterval(time: number, callback: () => void) {
    this.interval = setInterval(callback, time);
  }
  clearInterval() {
    clearInterval(this.interval);
  }
}
class MockIntervalService {
  callback;
  clearInterval = jasmine.createSpy('clearInterval');
  setInterval(time: number, callback: () => void): any {
    this.callback = callback;
    return null;
  }
  tick() {
    this.callback();
  }
}
@Component({
  template: '<span *ngIf="value">{{ value }}</span>',
})
class TestComponent implements OnInit, OnDestroy {
  value;
  constructor(private _intervalService: IntervalService) {}
  ngOnInit() {
    let counter = 0;
    this._intervalService.setInterval(1000, () => {
      this.value = ++counter;
    });
  }
  ngOnDestroy() {
    this._intervalService.clearInterval();
  }
}
describe('component: TestComponent', () => {
  let mockIntervalService: MockIntervalService;
  beforeEach(() => {
    mockIntervalService = new MockIntervalService();
    TestBed.configureTestingModule({
      imports: [ CommonModule ],
      declarations: [ TestComponent ],
      providers: [
        { provide: IntervalService, useValue: mockIntervalService }
      ]
    });
  });
  it('should set the value on each tick', () => {
    let fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    let el = fixture.debugElement.nativeElement;
    expect(el.querySelector('span')).toBeNull();
    mockIntervalService.tick();
    fixture.detectChanges();
    expect(el.innerHTML).toContain('1');
    mockIntervalService.tick();
    fixture.detectChanges();
    expect(el.innerHTML).toContain('2');
  });
  it('should clear the interval when component is destroyed', () => {
    let fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    fixture.destroy();
    expect(mockIntervalService.clearInterval).toHaveBeenCalled();
  });
});

我也遇到了同样的问题:特别是,当第三方服务从测试中调用setInterval()时,出现了这个错误:

错误:无法在异步区域测试中使用setInterval。

您可以模拟调用,但这并不总是可取的,因为您可能实际上想要测试与另一个模块的交互。

在我的案例中,我只使用Jasmine的(>=2.0)异步支持而不是Angulars的async():就解决了这个问题

it('Test MyAsyncService', (done) => {
  var myService = new MyAsyncService()
  myService.find().timeout(1000).toPromise() // find() returns Observable.
    .then((m: any) => { console.warn(m); done(); })
    .catch((e: any) => { console.warn('An error occured: ' + e); done(); })
  console.warn("End of test.")
});

使用Observable怎么样?https://github.com/angular/angular/issues/6539要测试它们,您应该使用.toPromise()方法