测试使用 setInterval 或 setTimeout 的 Angular2 组件

Posted

技术标签:

【中文标题】测试使用 setInterval 或 setTimeout 的 Angular2 组件【英文标题】:Testing Angular2 components that use setInterval or setTimeout 【发布时间】:2016-12-06 11:07:33 【问题描述】:

我有一个相当典型、简单的 ng2 组件,它调用服务来获取一些数据(轮播项目)。它还使用 setInterval 每隔 n 秒在 UI 中自动切换轮播幻灯片。它工作得很好,但是在运行 Jasmine 测试时出现错误:“无法在异步测试区域内使用 setInterval”。

我尝试将 setInterval 调用包装在 this.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;
    );
));

【问题讨论】:

【参考方案1】:

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

【讨论】:

我确实想确认那些正在编写新代码的人,Observable.timer() / Observable.interval() 可能是首选选项。在任何情况下,仍然存在需要处理 setTimeout/setInterval 的情况,例如导入的库。 我看到了一个非常相似的问题——fixture.whenStable() 永远无法解决——无论我使用的是Observable.interval 还是setInterval。夹具应该在间隔之间保持稳定,对吧?但它从未被认为是稳定的。【参考方案2】:

干净的代码是可测试的代码。 setInterval 有时很难测试,因为时机从来都不是完美的。您应该将setTimeout 抽象为一个可以为测试模拟的服务。在模拟中,您可以控制处理间隔的每个刻度。例如

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,您现在可以控制每个刻度,这在测试期间更容易推理。还有一个spy可以检查组件销毁时是否调用了clearInterval方法。

对于您的CarouselService,由于它也是异步的,请参阅this post 以获得良好的解决方案。

以下是使用前面提到的服务的完整示例(使用 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();
  );
);

【讨论】:

太棒了,这正是我需要知道的。我需要更改的一件事是在 MockIntervalService 中:“回调;”到“回调()”。否则会导致“TypeError: this.callback is not a function”。非常感谢! 这是个好主意,而且效果很好。制作一个匹配 setTimeout 和 setInterval 参数而不是交换它们的服务会更有意义。 此外,您不能使用该代码有两个间隔,因为如果您以后需要清除它们,您可能会丢失它们的 ID。为此,我会让 IntervalService 成为真实方法的副本。【参考方案3】:

我遇到了同样的问题:具体来说,当第三方服务从测试中调用 setInterval() 时出现此错误:

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

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

我只使用 Jasmine 的 (>=2.0) async support 而不是 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.")
);

【讨论】:

这只是一个解决方案,如果您不使用依赖于ComponentFixture#whenStable 的第三方测试助手(如Angular Material testbed harness)。

以上是关于测试使用 setInterval 或 setTimeout 的 Angular2 组件的主要内容,如果未能解决你的问题,请参考以下文章

关于this知识点

使用单例在无尽的 setInterval(,0) 中对 process.exit 的开玩笑测试调用

SetTimer API函数

为啥我要使用 RxJS 的 interval() 或 timer() 轮询而不是 window.setInterval()?

为啥 requestAnimationFrame 比 setInterval 或 setTimeout 更好

需要说明在 GNU C 中使用 settimer 和 alarm 功能的程序