javascript - Testa Angular2-komponenter som använder setInterval eller setTimeout


Jag har en ganska typisk, enkel ng2-komponent som anropar en tjänst för att hämta vissa data (karusellobjekt). Den använder också setInterval för att automatiskt byta karusellbilder i användargränssnittet var n:e sekund. Det fungerar helt fint, men när jag kör Jasmine-tester får jag felet: "Cannot use setInterval from within an async test zone".

Jag försökte kapsla in anropet till setInterval i this.zone.runOutsideAngular(() => {...}), men felet kvarstod. Jag skulle ha trott att ändringen av testet till att köras i en fakeAsync-zon skulle lösa problemet, men då får jag ett felmeddelande som säger att XHR-anrop inte är tillåtna inom fakeAsync-testzonen (vilket faktiskt är logiskt).

Hur kan jag använda både XHR-anropen som görs av tjänsten och intervallet, samtidigt som jag fortfarande kan testa komponenten? Jag använder ng2 rc4, projekt genererat av angular-cli. Tack på förhand.

Min kod från komponenten:

constructor(private carouselService: CarouselService) {
}

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

Och från Jasmine-specifikationen:

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

Svar #1

Ren kod är testbar kod. setInterval är ibland svårt att testa eftersom timingen aldrig är perfekt. Du bör abstrahera setTimeout till en tjänst som du kan mocka för testet. I mocken kan du ha kontroller för att hantera varje tick av intervallet. Till exempel

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

Med MockIntervalService kan du nu kontrollera varje tick, vilket är mycket lättare att förstå under testning. Det finns också en spion för att kontrollera att clearInterval-metoden kallas när komponenten förstörs.

För din CarouselService, eftersom den också är asynkron, se detta inlägg för en bra lösning.

Nedan är ett komplett exempel (med RC 6) som använder de tidigare nämnda tjänsterna.

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

Svar #2

Jag hade samma problem: specifikt fick jag detta fel när en tredjepartstjänst kallade setInterval() från ett test:

Fel: Kan inte använda setInterval inom ett asynkront zon-test.

Du kan imitera anropen, men det är inte alltid önskvärt, eftersom du faktiskt kanske vill testa interaktionen med en annan modul.

Jag löste det i mitt fall genom att helt enkelt använda Jasmines (>=2.0) asynkrona stöd istället för 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.")
});

Relaterade artiklar