测试使用 setInterval 或 setTimeout 的 Angular2 组件

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

I have a fairly typical, simple ng2 component that calls a service to get some data (carousel items). It also uses setInterval to auto-switch carousel slides in the UI every n seconds. It works just fine, but when running Jasmine tests I get the error: "Cannot use setInterval from within an async test zone".

我尝试将 setInterval 调用包装在 this.zone.runOutsideAngular(() => {...}) 中,但错误仍然存​​在.我原以为将测试更改为在 fakeAsync 区域中运行可以解决问题,但随后我收到一条错误消息,指出在 fakeAsync 测试区域内不允许进行 XHR 调用(这确实有意义).

I tried wrapping the setInterval call in this.zone.runOutsideAngular(() => {...}), but the error remained. I would've thought changing the test to run in fakeAsync zone would solve the problem, but then I get an error saying XHR calls are not allowed from within fakeAsync test zone (which does make sense).

我怎样才能同时使用服务发出的 XHR 调用和间隔,同时仍然能够测试组件?我正在使用 ng2 rc4,由 angular-cli 生成的项目.非常感谢.

How can I use both the XHR calls made by the service and the interval, while still being able to test the component? I'm using ng2 rc4, project generated by angular-cli. Many thanks in advance.

我的组件代码:

constructor(private carouselService: CarouselService) {
}

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

来自 Jasmine 规范:

And from the Jasmine spec:

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;
    });
}));

推荐答案

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

Clean code is testable code. setInterval is sometimes difficult to test because the timing is never perfect. You should abstract the setTimeout into a service that you can mock out for the test. In the mock you can have controls to handle each tick of the interval. For example

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 方法.

With the MockIntervalService you can now control each tick, which is so much more easy to reason about during testing. There's also a spy to check that the clearInterval method is called when the component is destroyed.

对于你的CarouselService,由于它也是异步的,请看这篇文章 一个好的解决方案.

For your CarouselService, since it is also asynchronous, please see this post for a good solution.

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

Below is a complete example (using RC 6) using the previously mentioned services.

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();
  });
});

相关文章