Neue Version von zone.js behebt Testing-Probleme von Observables

Christoph Höller March 20, 2017

Wie im Testing-Kapitel auf Seite 540 beschrieben besaß die bisheriger Implementierung von Angular einen schwerwiegenden Nachteil beim Testen von asynchronem Code mit Hilfe der Funktionen async und fakeAsync:

So war es aufgrund eines Implementierungdetails in zone.js bislang nicht möglich Code zu testen, der auf der setInterval-Funktion basiert (hierzu gehören auch die beiden RxJS-Operatoren debounceTime und interval). Seit der Version 0.78 ist dieser Fehler aber behoben, so dass Sie sowohl async als auch fakeAsync, sodass Sie die beiden Funktionen ab sofort ohne Einschränkungen zum Test Ihrer asynchronen Funktionalität verwenden können.

Die Installation der neuen zone.js-Version kann dabei einfach über den Befehl

npm i --save zone.js@^0.7.8

erfolgen.

Ein erstes Beispiel

Schauen Sie sich für ein besseres Verständnis der Funktionalität zunächst das folgende Beispiel an:

import {fakeAsync, tick, ...} from '@angular/core/testing';
it('should simulate time when using fakeAsync', fakeAsync(() => {
  let counter = 0;
  const interval = setInterval(() => {
    counter++;
  }, 200);
  tick(1000);
  expect(counter).toEqual(5);
  clearInterval(interval);
})); 

Beachten Sie hier zunächst einmal den äußeren Rumpf des Tests:

it('should simulate time when using fakeAsync', fakeAsync(() => {
  ...
})); 

Dadurch dass die Testausführung innerhalb der Funktion fakeAsync gekapselt wurde, wird der Code des Tests in einer “künstlichen Zeitzone” ausgeführt. Die Besonderheit in dieser Zone liegt darin, dass Sie Ihnen die Möglichkeit bietet die Zeit nach Belieben vorzuspulen. Doch dazu gleich mehr.

Innerhalb des Tests wird nun über die Zeilen:

let counter = 0;
const interval = setInterval(() => {
  counter++;
}, 200);

ein Interval gestartet, dass die Variable counter alle 200ms inkrementiert. Und nun wird es interessant: So haben Sie über die tick-Funktion die Möglichkeit künstlich in der Zeit nach vorne zu reisen. über den Aufruf

tick(1000);

lassen Sie also künstlich 1000ms vergehen, so dass der counter in dieser Zeit 5 mal erhöht wird und die Expectation

expect(counter).toEqual(5);

erfolgreich erfüllt wird!

Test der Typeahead Funktionalität

Wirklich interessant wird die Technik aber erst bei Test von echtem asynchronen Applikations-Code. Schauen Sie sich hierfür zunächst noch einmal die Implementierung der reaktiven Typeahead Suche aus der Datei task-list.component.ts.


export class TaskListComponent implements OnInit {

  tasks$: Observable<Task[]>;
  searchTerm = new FormControl();
  ...
  ngOnInit() {

    this.tasks$ = this.taskService.tasks$;

    const paramsStream = this.route.queryParams
      .map(params => decodeURI(params['query'] || ''))
      .do(query => this.searchTerm.setValue(query));

    const searchTermStream = this.searchTerm.valueChanges
      .debounceTime(400)
      .do(query => this.adjustBrowserUrl(query));

    Observable.merge(paramsStream, searchTermStream)
      .distinctUntilChanged()
      .switchMap(query =>  this.taskService.findTasks(query))
      .subscribe();
  }
}

Ich stelle Ihnen die Implementierung im Detail in Kapitel 12.2 ab Seite 465 vor. Für diesen Artikel ist es aber lediglich wichtig zu wissen, dass der Ausdruck:

const searchTermStream = this.searchTerm.valueChanges
  .debounceTime(400)
  .do(query => this.adjustBrowserUrl(query));
}

dafür sorgt, dass Nutzereingaben im Suchfeld automatisch in den Stream weitergeleitet werden, sobald der Nutzer für 400 Millisekunden keine Taste mehr gedrück hat. In Bezug auf den Test bedeutet das, dass Sie dort dafür sorgen müssen, dass nach der Eingabe der Nutzdaten (künstlich) mindestens 400 ms verstreichen. Das folgende Listing zeigt die Implementierung eines entsprechenden Tests:


  it('should call the backend to load tasks when user types in typeahead', fakeAsync(() => {
    const fixture = TestBed.createComponent(TaskListComponent);
    fixture.detectChanges();

    // Spy programmieren
    const spy = spyOn(taskService, 'findTasks');
    spy.and.returnValue(new BehaviorSubject({}));

    const searchInput = fixture.nativeElement.querySelector('#search-tasks');

    const searchTerm = 'Entwickler';
    setInputValue(searchInput, searchTerm);

    //lasse 400 Millisekunden verstreichen
    tick(400);

    // Spy auswerten
    const findArguments = spy.calls.mostRecent().args;
    expect(findArguments[0]).toBe(searchTerm);
  }));
</code></pre>

Sie finden diesen Test in den aktualisierten Sourcen im Projekt project-manager-reactive in der Datei task-list.component.spec.ts.

Über die beiden Zeilen

 

       // Spy programmieren
     const spy = spyOn(taskService, 'findTasks');
     spy.and.returnValue(new BehaviorSubject({}));

 </code></pre>
programmieren Sie hier zunächst einen Jasmine-Spy der die findTasks-Methode des TaskService untersucht. Anschließend wird über die Zeilen
 

     const searchInput = fixture.nativeElement.querySelector('#search-tasks');
     const searchTerm = 'Entwickler';
     setInputValue(searchInput, searchTerm);
 
der Wert des Input Feldes mit der id 'search-tasks' auf "Entwickler" gesetzt. Mit Hilfe der tick-Funktion lassen Sie nun 400 Millisekunden verstreichen

     tick(400);
 
und überprüfen anschließend, mit Hilfe der calls.mostRecent()-Funktion des Spy, dass die findTasks-Methode mit dem Parameter "Entwickler" aufgerufen wurde.

     // Spy auswerten
     const findArguments = spy.calls.mostRecent().args;
     expect(findArguments[0]).toBe(searchTerm);
 
Durch den Bugfix von **zone.js** haben Sie nun also die Möglichkeit mit Hilfe von fakeAsync und tick sehr elegante Tests für auf Observables basierende asynchrone Funktionalität bereitzustellen!