it-swarm.com.de

Testen von Angular2-Komponenten, die setInterval oder setTimeout verwenden

Ich habe eine ziemlich typische, einfache ng2-Komponente, die einen Dienst aufruft, um Daten abzurufen (Karussellelemente). Außerdem wird setInterval verwendet, um Karussell-Folien in der Benutzeroberfläche alle n Sekunden automatisch umzuschalten. Es funktioniert einwandfrei, aber beim Ausführen von Jasmine-Tests erhalte ich die Fehlermeldung: "setInterval kann nicht in einer asynchronen Testzone verwendet werden".

Ich habe versucht, den Aufruf von setInterval in this.zone.runOutsideAngular (() => {...}) zu packen, aber der Fehler blieb bestehen. Ich hätte gedacht, dass eine Änderung des Tests in der fakeAsync-Zone das Problem lösen würde, aber dann erhalte ich die Fehlermeldung, dass XHR-Aufrufe in der fakeAsync-Testzone nicht zulässig sind (was Sinn macht).

Wie kann ich sowohl die XHR-Aufrufe des Dienstes als auch das Intervall verwenden, ohne die Komponente zu testen? Ich benutze ng2 rc4, ein Projekt, das von angle-cli generiert wurde. Vielen Dank im Voraus.

Mein Code aus der Komponente:

constructor(private carouselService: CarouselService) {
}

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

Und aus der Jasminspezifikation:

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

Clean Code ist testbarer Code. setInterval ist manchmal schwierig zu testen, da das Timing nie perfekt ist. Sie sollten die setTimeout in einen Dienst abstrahieren, den Sie für den Test verspotten können. Im Mock können Sie Steuerelemente haben, um jeden Tick des Intervalls zu behandeln. Zum Beispiel

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

Mit der MockIntervalService können Sie jetzt jeden Tick steuern, was beim Testen viel einfacher zu überlegen ist. Es gibt auch einen Spion, der überprüft, ob die Methode clearInterval aufgerufen wird, wenn die Komponente zerstört wird.

Da Ihre CarouselService auch asynchron ist, finden Sie in diesem Beitrag eine gute Lösung.

Nachfolgend finden Sie ein vollständiges Beispiel (unter Verwendung von RC 6) mit den zuvor genannten Diensten.

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

Ich hatte das gleiche Problem: speziell, diesen Fehler zu bekommen, wenn ein Drittanbieter setInterval() von einem Test aufrief:

Fehler: setInterval kann nicht innerhalb eines asynchronen Zonentests verwendet werden.

Sie können die Aufrufe verspotten, was jedoch nicht immer wünschenswert ist, da Sie die Interaktion mit einem anderen Modul möglicherweise testen möchten.

Ich habe es in meinem Fall gelöst, indem ich Jasmines (> = 2.0) asynchrone Unterstützung anstelle von Angulars async() verwendet habe:

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

Was ist mit dem Observable? https://github.com/angular/angular/issues/6539 Zum Testen sollten Sie die .toPromise () -Methode verwenden

1
jacopobeschi