Тестирование компонентов Angular2, которые используют setInterval или setTimeout

У меня есть довольно типичный, простой компонент ng2, который вызывает сервис для получения некоторых данных (элементы карусели). Он также использует setInterval для автоматического переключения слайдов карусели в пользовательском интерфейсе каждые n секунд. Это работает просто отлично, но при запуске тестов Jasmine я получаю сообщение об ошибке: «Невозможно использовать setInterval из асинхронной тестовой зоны».

Я попытался обернуть вызов setInterval в this.zone.runOutsideAngular (() => {...}), но ошибка осталась. Я бы подумал, что изменение теста для запуска в зоне fakeAsync решит проблему, но тогда я получаю сообщение об ошибке, в котором говорится, что вызовы XHR не разрешены из зоны тестирования fakeAsync (что имеет смысл).

Как я могу использовать как вызовы XHR, сделанные службой, так и интервал, все еще имея возможность протестировать компонент? Я использую ng2 rc4, проект, созданный 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;
    });
}));

Ответы на вопрос(3)

У меня была та же проблема: в частности, получение этой ошибки при вызове сторонней службыsetInterval() из теста:

Ошибка: невозможно использовать setInterval из теста асинхронной зоны.

Вы можете смоделировать вызовы, но это не всегда желательно, так как вы можете проверить взаимодействие с другим модулем.

Я решил это в моем случае, просто используя Jasmine's (> = 2.0)асинхронная поддержка вместо ангуляров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.")
});
Решение Вопроса

Чистый код - это тестируемый код.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();
  });
});
 Dunos20 апр. 2017 г., 12:46
Кроме того, у вас не может быть двух интервалов с этим кодом, так как вы можете потерять их идентификаторы, если вам потребуется очистить их позже. Для этого я бы позволил IntervalService просто быть копией реальных методов.
 Siimo Raba21 окт. 2016 г., 13:54
Гениально, это именно то, что мне нужно было знать. Одна вещь, которую мне нужно было изменить, была в MockIntervalService: "callback;" на "обратный вызов () {}". В противном случае это приведет к «TypeError: this.callback не является функцией». Большое спасибо!
 jsgoupil03 дек. 2016 г., 01:02
Это отличная идея и прекрасно работает. Было бы разумнее создать сервис, который соответствует аргументам setTimeout и setInterval, а не обменивать их.

Как насчет использования Observable?https://github.com/angular/angular/issues/6539 Чтобы проверить их, вы должны использовать метод .toPromise ()

 Siimo Raba15 дек. 2016 г., 11:46
Я действительно хотел бы подтвердить, что для тех, кто пишет новый код, Observable.timer () / Observable.interval () может быть предпочтительным вариантом. В любом случае все еще будут ситуации, когда нужно иметь дело с setTimeout / setInterval, например, импортированные библиотеки.

Ваш ответ на вопрос