web-dev-qa-db-fra.com

Test des composants Angular2 utilisant setInterval ou setTimeout

J'ai un composant ng2 assez typique et simple qui appelle un service pour obtenir des données (éléments du carrousel). Il utilise également setInterval pour permuter automatiquement les diapositives du carrousel dans l'interface utilisateur toutes les n secondes. Cela fonctionne très bien, mais lors de l'exécution des tests de Jasmine, j'obtiens le message d'erreur: "Impossible d'utiliser setInterval à partir d'une zone de test asynchrone".

J'ai essayé d'encapsuler l'appel setInterval dans this.zone.runOutsideAngular (() => {...}), mais l'erreur est restée. J'aurais pensé que changer le test pour qu'il fonctionne dans la zone fakeAsync résoudrait le problème, mais j'obtiens une erreur disant que les appels XHR ne sont pas autorisés depuis la zone de test fakeAsync (ce qui est logique).

Comment utiliser les appels XHR émis par le service et l'intervalle tout en testant le composant? J'utilise ng2 rc4, projet généré par angular-cli. Merci d'avance.

Mon code 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;
    });
}));
13
Siimo Raba

Un code propre est un code testable. setInterval est parfois difficile à tester car le timing n'est jamais parfait. Vous devez résumer la setTimeout dans un service que vous pouvez simuler pour le test. Dans la maquette, 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 tick, ce qui est beaucoup plus facile à raisonner lors des tests. Il y a aussi 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, veuillez consulter this post pour une bonne solution.

Vous trouverez ci-dessous un exemple complet (avec RC 6) 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();
  });
});
10
Paul Samsotha

J'ai eu le même problème: en particulier, obtenir cette erreur quand un service tiers appelait setInterval() à partir d'un test:

Erreur: impossible d'utiliser setInterval à partir d'un test de zone asynchrone.

Vous pouvez simuler les appels, mais ce n'est pas toujours souhaitable, car vous souhaiterez peut-être tester l'interaction avec un autre module.

Je l'ai résolu dans mon cas en utilisant simplement Jasmine (> = 2.0) support async au lieu de 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.")
});
3
spinkus

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

1
jacopobeschi