首页 文章

测试使用setInterval或setTimeout的Angular2组件

提问于
浏览
13

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

我尝试在this.zone.runOutsideAngular(()=> 中包装setInterval调用,但错误仍然存在 . 我认为改变测试以在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;
    });
}));

3 回答

  • 10

    清洁代码是可测试的代码 . 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();
      });
    });
    
  • 3

    我遇到了同样的问题:具体来说,当第三方服务从测试中调用_1310442时,得到这个错误:

    错误:无法在异步区域测试中使用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.")
    });
    
  • 1

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

相关问题