Test des composants Angular2 qui utilisent setInterval ou setTimeout

J'ai un composant ng2 simple et typique qui appelle un service pour obtenir des données (articles de carrousel). Il utilise également setInterval pour auto-basculer les diapositives de carrousel dans l'UI toutes les n secondes. Cela fonctionne très bien, mais lors de l'exécution des tests Jasmine, j'ai l'erreur: "Impossible d'utiliser setInterval dans une zone de test asynchrone".

J'ai essayé d'enrouler l'appel setInterval dans this.zone.runOutsideAngular (() => {…}), mais l'erreur est restée. J'aurais pensé que changer le test pour fonctionner dans la zone faussesAsync résoudrait le problème, mais je reçois une erreur indiquant que les appels XHR ne sont pas autorisés dans la zone de test fauxAsync (ce qui est logique).

Comment puis-je utiliser à la fois les appels XHR effectués par le service et l'intervalle, tout en pouvant tester le composant? J'utilise ng2 rc4, projet généré par angular-cli. Merci d'avance.

Mon code à partir du composant:

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

Et de la spécification 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; }); })); 

Le code propre est un code testable. setInterval est parfois difficile à tester car le timing n'est jamais parfait. Vous devez setTimeout le setTimeout dans un service que vous pouvez choisir pour le test. Dans la simulation, vous pouvez avoir des contrôles pour gérer chaque tick de l'intervalle. Par exemple

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

Avec MockIntervalService vous pouvez maintenant contrôler chaque coche, ce qui est beaucoup plus facile à expliquer au cours des tests. Il existe également un espion pour vérifier que la méthode clearInterval est appelée lorsque le composant est détruit.

Pour votre CarouselService , car il est également asynchrone, consultez cette publication pour une bonne solution.

Voici un exemple complet (en utilisant RC 6) en utilisant les services mentionnés précédemment.

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

J'ai eu le même problème: spécifiquement, j'éprouvais cette erreur lorsqu'un service tiers appelait setInterval() partir d'un test:

Erreur: Impossible d'utiliser setInterval dans un test de zone asynchrone.

Vous pouvez vous moquer des appels, mais cela n'est pas toujours souhaitable, car vous pouvez réellement essayer de tester l'interaction avec un autre module.

Je l'ai résolu dans mon cas en utilisant simplement le support asynchimique (> = 2,0) de Jasmine au lieu d'Async async() de Angulars:

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

Qu'en est-il de l'utilisation de l'Observable? https://github.com/angular/angular/issues/6539 Pour les tester, vous devez utiliser la méthode .toPromise ()