web-dev-qa-db-ja.com

setIntervalまたはsetTimeoutを使用するAngular2コンポーネントのテスト

サービスを呼び出してデータ(カルーセルアイテム)を取得する、かなり典型的な単純なng2コンポーネントがあります。また、setIntervalを使用して、UIのカルーセルスライドをn秒ごとに自動切り替えします。正常に動作しますが、Jasmineテストを実行すると、「非同期テストゾーン内からsetIntervalを使用できません」というエラーが表示されます。

SetInterval呼び出しをthis.zone.runOutsideAngular(()=> {...})でラップしようとしましたが、エラーが残りました。 fakeAsyncゾーンで実行するようにテストを変更すると問題が解決すると思いましたが、fakeAsyncテストゾーン内からのXHR呼び出しは許可されていないというエラーが表示されます(これは理にかなっています)。

コンポーネントをテストしながら、サービスによって行われたXHR呼び出しと間隔の両方を使用するにはどうすればよいですか?私はng2rc4、angular-cliによって生成されたプロジェクトを使用しています。よろしくお願いします。

コンポーネントからの私のコード:

constructor(private carouselService: CarouselService) {
}

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

そして、ジャスミンの仕様から:

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;
    });
}));
14
Siimo Raba

クリーンコードはテスト可能なコードです。 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を使用すると、各ティックを制御できるようになりました。これは、テスト中に推論するのがはるかに簡単です。コンポーネントが破棄されたときに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();
  });
});
12
Paul Samsotha

同じ問題がありました。具体的には、サードパーティのサービスがテストからsetInterval()を呼び出しているときに、このエラーが発生しました。

エラー:非同期ゾーンテスト内からsetIntervalを使用することはできません。

呼び出しをモックアウトすることはできますが、実際には別のモジュールとの相互作用をテストしたい場合があるため、これが常に望ましいとは限りません。

私の場合、Angularsのasync()の代わりにJasmineの(> = 2.0) 非同期サポート を使用することで解決しました。

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.")
});
3
spinkus

Observableの使用はどうですか? https://github.com/angular/angular/issues/6539 それらをテストするには、.toPromise()メソッドを使用する必要があります

1
Jacopo Beschi