Angular: Testen von asynchronem Material in der fakedAsync-Zone VS. Bereitstellung von benutzerdefinierten Schedulern

Ich wurde oft nach der „Fake Zone“ und ihrer Verwendung gefragt. Aus diesem Grund habe ich beschlossen, diesen Artikel zu schreiben, um meine Beobachtungen zu detaillierten „fakeAsync“ -Tests mitzuteilen.

Die Zone ist ein entscheidender Teil des Ökosystems Angular. Man könnte gelesen haben, dass die Zone selbst nur eine Art „Ausführungskontext“ ist. Angular monkeypatcht die globalen Funktionen wie setTimeout oder setInterval, um Funktionen abzufangen, die nach einer gewissen Verzögerung (setTimeout) oder in regelmäßigen Abständen (setInterval) ausgeführt werden.

Es ist wichtig zu erwähnen, dass in diesem Artikel nicht gezeigt wird, wie mit setTimeout-Hacks umgegangen wird. Da Angular RxJs stark nutzt, die auf systemeigenen Timing-Funktionen basieren (Sie sind vielleicht überrascht, aber es ist wahr), verwendet es zone als komplexes, aber leistungsstarkes Tool, um alle asynchronen Aktionen aufzuzeichnen, die sich auf den Status der Anwendung auswirken könnten. Angular fängt sie ab, um festzustellen, ob sich noch Arbeit in der Warteschlange befindet. Je nach Uhrzeit wird die Warteschlange geleert. Höchstwahrscheinlich ändern die abgelassenen Tasks die Werte der Komponentenvariablen. Infolgedessen wird die Vorlage erneut gerendert.

Nun, all das asynchrone Zeug ist nicht das, worüber wir uns sorgen müssen. Es ist einfach schön zu verstehen, was unter der Haube passiert, weil es hilft, effektive Unit-Tests zu schreiben. Darüber hinaus hat die testgetriebene Entwicklung einen enormen Einfluss auf den Quellcode („TDDs Ursprünge waren der Wunsch, starke automatische Regressionstests zu erhalten, die das evolutionäre Design unterstützten. Währenddessen stellten die Praktiker fest, dass das Schreiben von Tests den Designprozess zum ersten Mal erheblich verbesserte. “Martin Fowler, https://martinfowler.com/articles/mocksArentStubs.html, 09/2017).

Als Ergebnis all dieser Bemühungen können wir die Zeit verschieben, wenn wir zu einem bestimmten Zeitpunkt auf Zustand prüfen müssen.

fakeAsync / tick outline

Die Angular-Dokumentation gibt an, dass fakeAsync (https://angular.io/guide/testing#fake-async) eine linearere Codierung ermöglicht, da Versprechungen wie .whenStable (). Then (…) beseitigt werden.

Der Code im fakeAsync-Block sieht folgendermaßen aus:

Häkchen (100); // Warten Sie, bis die erste Aufgabe erledigt ist
fixture.detectChanges (); // Ansicht mit Zitat aktualisieren
Tick(); // Warten Sie, bis die zweite Aufgabe erledigt ist
fixture.detectChanges (); // Ansicht mit Zitat aktualisieren

Die folgenden Ausschnitte geben einen Einblick in die Funktionsweise von fakeAsync.

setTimeout / setInterval werden hier verwendet, weil sie deutlich zeigen, wann die Funktionen in der Zone fakeAsync ausgeführt werden. Sie können erwarten, dass diese „es“ -Funktion wissen muss, wann der Test abgeschlossen ist (in Jasmine nach Argumenten geordnet: Funktion), aber dieses Mal verlassen wir uns auf den fakeAsync-Begleiter, anstatt irgendeine Art von Rückruf zu verwenden:

it ('Entleert die Zone Task für Task', fakeAsync (() => {
        setTimeout (() => {
            sei i = 0;
            const handle = setInterval (() => {
                if (i ++ === 5) {
                    clearInterval (handle);
                }
            }, 1000);
        }, 10000);
}));

Es beklagt sich lautstark, weil sich noch einige "Timer" (= setTimeouts) in der Warteschlange befinden:

Fehler: 1 Timer noch in der Warteschlange.

Es ist offensichtlich, dass wir die Zeit verschieben müssen, um die Timeout-Funktion auszuführen. Wir hängen das parametrierte „Häkchen“ mit 10 Sekunden an:

Häkchen (10000);

Hugh? Der Fehler wird verwirrender. Jetzt schlägt der Test wegen der eingereihten „periodischen Timer“ (= setIntervals) fehl:

Fehler: 1 periodischer Timer ist noch in der Warteschlange.

Da wir eine Funktion in die Warteschlange gestellt haben, die jede Sekunde ausgeführt werden muss, müssen wir auch die Zeit durch erneutes Verwenden des Häkchens verschieben. Die Funktion beendet sich nach 5 Sekunden von selbst. Deshalb müssen wir weitere 5 Sekunden hinzufügen:

Häkchen (15000);

Jetzt ist der Test bestanden. Es ist erwähnenswert, dass die Zone parallel laufende Aufgaben erkennt. Erweitern Sie einfach die Timeout-Funktion um einen weiteren Aufruf von setInterval.

it ('Entleert die Zone Task für Task', fakeAsync (() => {
    setTimeout (() => {
        sei i = 0;
        const handle = setInterval (() => {
            if (++ i === 5) {
                clearInterval (handle);
            }
        }, 1000);
        sei j = 0;
        const handle2 = setInterval (() => {
            if (++ j === 3) {
                clearInterval (handle2);
            }
        }, 1000);
    }, 10000);
    Häkchen (15000);
}));

Der Test ist noch nicht abgeschlossen, da beide setIntervals gleichzeitig gestartet wurden. Beide sind erledigt, wenn 15 Sekunden vergangen sind:

fakeAsync / tick in action

Jetzt wissen wir, wie das fakeAsync / tick-Zeug funktioniert. Lassen Sie es für einige sinnvolle Dinge verwenden.

Entwickeln wir ein Feld mit Vorschlägen, das diese Anforderungen erfüllt:

  • es holt sich das Ergebnis von einer API (Dienst)
  • es drosselt die Benutzereingabe, um auf den endgültigen Suchbegriff zu warten (es verringert die Anzahl der Anforderungen); DEBOUNCING_VALUE = 300
  • Es zeigt das Ergebnis in der Benutzeroberfläche und gibt die entsprechende Meldung aus
  • Der Komponententest berücksichtigt die asynchrone Natur des Codes und testet das ordnungsgemäße Verhalten des suggerartigen Felds in Bezug auf die verstrichene Zeit

Wir enden mit diesen Testszenarien:

describe ('on search', () => {
    it ('löscht das vorherige Ergebnis', fakeAsync (() => {
    }));
    es ('gibt das Startsignal aus', fakeAsync (() => {
    }));
    it ('drosselt die möglichen Treffer der API auf 1 Anforderung pro DEBOUNCING_VALUE Millisekunden', fakeAsync (() => {
    }));
});
beschreiben ('auf Erfolg', () => {
    it ('ruft die Google API auf', fakeAsync (() => {
    }));
    es ('gibt das Erfolgssignal mit der Anzahl der Übereinstimmungen aus', fakeAsync (() => {
    }));
    it ('zeigt die Titel im Vorschlagsfeld', fakeAsync (() => {
    }));
});
describe ('on error', () => {
    es ('gibt das Fehlersignal aus', fakeAsync (() => {
    }));
});

In „on search“ warten wir nicht auf das Suchergebnis. Wenn der Benutzer eine Eingabe vornimmt (zum Beispiel „Lon“), müssen die vorherigen Optionen gelöscht werden. Wir erwarten, dass die Optionen leer sind. Außerdem muss die Benutzereingabe gedrosselt werden, beispielsweise um einen Wert von 300 Millisekunden. In Bezug auf die Zone wird eine 300-Millis-Mikrotask in die Warteschlange gestellt.

Beachten Sie, dass ich der Kürze halber einige Details weglasse:

  • Das Test-Setup ist so ziemlich das gleiche wie in Angular Docs
  • Die apiService-Instanz wird über fixture.debugElement.injector (…) injiziert.
  • SpecUtils löst die benutzerbezogenen Ereignisse wie Eingabe und Fokus aus
beforeEach (() => {
    spyOn (apiService, 'query'). and.returnValue (Observable.of (queryResult));
});
fit ('löscht das vorherige Ergebnis', fakeAsync (() => {
    comp.options = ['nicht leer'];
    SpecUtils.focusAndInput ('Lon', Fixture, 'Input');
    tick (DEBOUNCING_VALUE);
    fixture.detectChanges ();
    expect (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);
}));

Der Komponentencode, der versucht, den Test zu erfüllen:

ngOnInit () {
    this.control.valueChanges.debounceTime (300) .subscribe (value => {
        this.options = [];
        this.suggest (value);
    });
}
suggest (q: string) {
    this.googleBooksAPI.query (q) .subscribe (result => {
// ...
    }, () => {
// ...
    });
}

Gehen wir den Code Schritt für Schritt durch:

Wir spionieren die Abfragemethode apiService aus, die wir in der Komponente aufrufen werden. Die Variable queryResult enthält einige Scheindaten wie „Hamlet“, „Macbeth“ und „King Lear“. Am Anfang erwarten wir, dass die Optionen leer sind, aber wie Sie vielleicht bemerkt haben, wird die gesamte fakeAsync-Warteschlange mit einem Häkchen (DEBOUNCING_VALUE) geleert, und daher enthält die Komponente auch das Endergebnis von Shakespeares Schriften:

Erwartet, dass 3 0 ist, war [Hamlet, Macbeth, King Lear].

Wir benötigen eine Verzögerung für die Dienstabfrageanforderung, um einen asynchronen Zeitablauf zu emulieren, der vom API-Aufruf verbraucht wird. Addieren wir 5 Sekunden Verzögerung (REQUEST_DELAY = 5000) und kreuzen Sie (5000) an.

beforeEach (() => {
    spyOn (apiService, 'query'). and.returnValue (Observable.of (queryResult) .delay (1000));
});

fit ('löscht das vorherige Ergebnis', fakeAsync (() => {
    comp.options = ['nicht leer'];
    SpecUtils.focusAndInput ('Lon', Fixture, 'Input');
    tick (DEBOUNCING_VALUE);
    fixture.detectChanges ();
    expect (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);
    tick (REQUEST_DELAY);
}));

Meiner Meinung nach sollte dieses Beispiel funktionieren, aber Zone.js behauptet, dass sich noch etwas Arbeit in der Warteschlange befindet:

Fehler: 1 periodischer Timer ist noch in der Warteschlange.

An diesem Punkt müssen wir tiefer gehen, um die Funktionen zu sehen, von denen wir vermuten, dass sie in der Zone stecken bleiben. Das Setzen einiger Haltepunkte ist der richtige Weg:

Debuggen von FakeAsync-Zone

Geben Sie dies dann in der Befehlszeile ein

_fakeAsyncTestZoneSpec._scheduler._schedulerQueue [0] .args [0] [0]

oder überprüfen Sie den Inhalt der Zone wie folgt:

hmmm, AsyncSchedulers Flush-Methode ist immer noch in der Warteschlange ... warum?

Der Name der Funktion, die in die Warteschlange gestellt wird, lautet AsyncSchedulers Flush-Methode.

public flush (Aktion: AsyncAction ): void {
  const {actions} = this;
  if (this.active) {
    actions.push (Aktion);
    Rückkehr;
  }
  let error: any;
  this.active = true;
  machen {
    if (error = action.execute (action.state, action.delay)) {
      brechen;
    }
  } while (action = actions.shift ()); // Die Scheduler-Warteschlange erschöpfen
  this.active = false;
  if (Fehler) {
    while (action = actions.shift ()) {
      action.unsubscribe ();
    }
    Fehler werfen;
  }
}

Nun fragen Sie sich vielleicht, was mit dem Quellcode oder der Zone selbst nicht stimmt.

Das Problem ist, dass die Zone und unsere Zecken nicht synchron sind.

Die Zone selbst hat die aktuelle Zeit (2017), während das Häkchen die Aktion verarbeiten möchte, die am 01.01.1970 + 300 Millis + 5 Sekunden geplant ist.

Der Wert des asynchronen Schedulers bestätigt Folgendes:

{async as AsyncScheduler} aus 'rxjs / scheduler / async' importieren;
// platziere dies irgendwo innerhalb des „es“
console.info (AsyncScheduler.now ());
// → 1503235213879

AsyncZoneTimeInSyncKeeper zur Rettung

Eine mögliche Lösung hierfür ist ein Keep-In-Sync-Dienstprogramm wie das folgende:

Exportklasse AsyncZoneTimeInSyncKeeper {
    Zeit = 0;
    Konstrukteur() {
        spyOn (AsyncScheduler, 'now'). and.callFake (() => {
            / * tslint: deaktiviere die nächste Zeile * /
            console.info ('time', this.time);
            Geben Sie diesmal zurück.
        });
    }
    tick (Zeit ?: Nummer) {
        if (Zeittyp! == 'undefined') {
            this.time + = time;
            tick (this.time);
        } else {
            Tick();
        }
    }
}

Es verfolgt die aktuelle Zeit, die von now () zurückgegeben wird, wenn der asynchrone Scheduler aufgerufen wird. Dies funktioniert, weil die Funktion tick () dieselbe aktuelle Uhrzeit verwendet. Sowohl der Scheduler als auch die Zone verwenden dieselbe Zeit.

Ich empfehle, den timeInSyncKeeper in der beforeEach-Phase zu instanziieren:

describe ('on search', () => {
    let timeInSyncKeeper;
    beforeEach (() => {
        timeInSyncKeeper = neuer AsyncZoneTimeInSyncKeeper ();
    });
});

Betrachten wir nun die Verwendung des Zeitsynchronisations-Keepers. Denken Sie daran, dass wir dieses Timing-Problem angehen müssen, da das Textfeld entprellt ist und die Anforderung einige Zeit in Anspruch nimmt.

describe ('on search', () => {
    let timeInSyncKeeper;
    beforeEach (() => {
        timeInSyncKeeper = neuer AsyncZoneTimeInSyncKeeper ();
        spyOn (apiService, 'query'). and.returnValue (Observable.of (queryResult) .delay (REQUEST_DELAY));
    });
    it ('löscht das vorherige Ergebnis', fakeAsync (() => {
        comp.options = ['nicht leer'];
        SpecUtils.focusAndInput ('Lon', Fixture, 'Input');
        timeInSyncKeeper.tick (DEBOUNCING_VALUE);
        fixture.detectChanges ();
        expect (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);
        timeInSyncKeeper.tick (REQUEST_DELAY);
    }));
    // ...
});

Lassen Sie uns dieses Beispiel Zeile für Zeile durchgehen:

  1. Instanz des synchronisierten Keepers instanziieren
timeInSyncKeeper = neuer AsyncZoneTimeInSyncKeeper ();

2. Lassen Sie die Methode apiService.query mit dem Ergebnis queryResult antworten, nachdem REQUEST_DELAY verstrichen ist. Angenommen, die Abfragemethode ist langsam und antwortet nach REQUEST_DELAY = 5000 Millisekunden.

spyOn (apiService, 'query'). and.returnValue (Observable.of (queryResult) .delay (REQUEST_DELAY));

3. Stellen Sie sich vor, dass im Vorschlagsfeld eine Option „Nicht leer“ vorhanden ist

comp.options = ['nicht leer'];

4. Gehen Sie zum Eingabefeld im nativen Element des Geräts und fügen Sie den Wert „Lon“ ein. Dies simuliert die Benutzerinteraktion mit dem Eingabefeld.

SpecUtils.focusAndInput ('Lon', Fixture, 'Input');

5. Lassen Sie den Zeitraum DEBOUNCING_VALUE in der gefälschten asynchronen Zone (DEBOUNCING_VALUE = 300 Millisekunden) vergehen.

timeInSyncKeeper.tick (DEBOUNCING_VALUE);

6. Erkennen Sie Änderungen und rendern Sie die HTML-Vorlage erneut.

fixture.detectChanges ();

7. Das Optionsfeld ist jetzt leer!

expect (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);

Dies bedeutet, dass die in den Komponenten verwendeten beobachtbaren valueChanges zur richtigen Zeit ausgeführt wurden. Beachten Sie, dass die Funktion debounceTime-d ausgeführt wird

value => {
    this.options = [];
    this.onEvent.emit ({signal: SuggestSignal.start});
    this.suggest (value);
}

hat eine andere Aufgabe in die Warteschlange gestellt, indem die Methode suggest aufgerufen wurde:

suggest (q: string) {
    if (! q) {
        Rückkehr;
    }
    this.googleBooksAPI.query (q) .subscribe (result => {
        if (result) {
            this.options = result.items.map (item => item.volumeInfo);
            this.onEvent.emit ({signal: SuggestSignal.success, totalItems: result.totalItems});
        } else {
            this.onEvent.emit ({signal: SuggestSignal.success, totalItems: 0});
        }
    }, () => {
        this.onEvent.emit ({signal: SuggestSignal.error});
    });
}

Erinnern Sie sich einfach an die Google Books API-Abfragemethode, die nach 5 Sekunden antwortet.

8. Zum Schluss müssen wir noch einmal REQUEST_DELAY = 5000 Millisekunden ankreuzen, um die Zonenwarteschlange zu leeren. Für die Observable, die wir in der suggest-Methode abonnieren, ist REQUEST_DELAY = 5000 erforderlich.

timeInSyncKeeper.tick (REQUEST_DELAY);

fakeAsync…? Warum? Es gibt Scheduler!

Die ReactiveX-Experten könnten argumentieren, dass wir Test-Scheduler verwenden könnten, um die Observablen testbar zu machen. Dies ist für Winkelanwendungen möglich, hat jedoch einige Nachteile:

  • Sie müssen sich mit der inneren Struktur von Observablen, Operatoren usw. vertraut machen.
  • Was ist, wenn Ihre Anwendung einige hässliche Problemumgehungen für setTimeout enthält? Sie werden von den Schedulern nicht verarbeitet.
  • Das Wichtigste: Ich bin sicher, dass Sie Scheduler nicht in Ihrer gesamten Anwendung verwenden möchten. Sie möchten den Produktionscode nicht mit Ihren Komponententests mischen. Sie möchten so etwas nicht tun:
const testScheduler;
if (environment.test) {
    testScheduler = new YourTestScheduler ();
}
beobachtbar lassen;
if (testScheduler) {
    observable = Observable.of (‘value’). delay (1000, testScheduler)
} else {
    observable = Observable.of (‘value’). delay (1000);
}

Dies ist keine praktikable Lösung. Meiner Meinung nach besteht die einzig mögliche Lösung darin, den Test Scheduler durch die Bereitstellung von „Proxies“ für die realen Rxjs-Methoden zu „injizieren“. Zu berücksichtigen ist auch, dass sich überschreibende Methoden negativ auf die verbleibenden Komponententests auswirken können. Deshalb werden wir Jasmines Spione einsetzen. Spione werden nach jedem davon gelöscht.

Die Funktion monkeypatchScheduler umschließt die ursprüngliche Rxjs-Implementierung mithilfe eines Spions. Der Spion übernimmt die Argumente der Methode und hängt gegebenenfalls den testScheduler an.

{IScheduler} aus 'rxjs / Scheduler' importieren;
{Observable} aus 'rxjs / Observable' importieren;
Deklariere var spyOn: Function;
Exportfunktion monkeypatchScheduler (Scheduler: IScheduler) {
    let observableMethods = ['concat', 'defer', 'empty', 'forkJoin', 'if', 'interval', 'merge', 'of', 'range', 'throw',
        'Postleitzahl'];
    let operatorMethods = ['buffer', 'concat', 'delay', 'distinct', 'do', 'every', 'last', 'merge', 'max', 'take',
        'timeInterval', 'lift', 'debounceTime'];
    let injectFn = function (Basis: any, Methoden: string []) {
        methods.forEach (method => {
            const orig = base [Methode];
            if (typeof orig === 'function') {
                spyOn (Basis, Methode) .and.callFake (function () {
                    let args = Array.prototype.slice.call (Argumente);
                    if (args [args.length - 1] && typeof args [args.length - 1] .now === 'function') {
                        args [args.length - 1] = Scheduler;
                    } else {
                        args.push (Scheduler);
                    }
                    return orig.apply (this, args);
                });
            }
        });
    };
    injectFn (Observable, observableMethods);
    injectFn (Observable.prototype, operatorMethods);
}

Von nun an führt der testScheduler alle Arbeiten in Rxjs aus. Es werden weder setTimeout / setInterval noch andere asynchrone Dinge verwendet. Es besteht keine Notwendigkeit mehr für fakeAsync.

Jetzt benötigen wir eine Test Scheduler-Instanz, die wir an monkeypatchScheduler übergeben möchten.

Es verhält sich sehr ähnlich wie der Standard-TestScheduler, bietet jedoch eine Rückrufmethode onAction. Auf diese Weise wissen wir, welche Aktion nach welchem ​​Zeitraum ausgeführt wurde.

Exportklasse SpyingTestScheduler erweitert VirtualTimeScheduler {
    spyFn: (actionName: string, delay: number, error ?: any) => void;
    Konstrukteur() {
        super (VirtualAction, defaultMaxFrame);
    }
    onAction (spyFn: (actionName: string, delay: number, error ?: any) => void) {
        this.spyFn = spyFn;
    }
    Flush () {
        const {actions, maxFrames} = this;
        let error: any, action: AsyncAction ;
        while ((action = actions.shift ()) && (this.frame = action.delay) <= maxFrames) {
            let stateName = this.detectStateName (Aktion);
            let delay = action.delay;
            if (error = action.execute (action.state, action.delay)) {
                if (this.spyFn) {
                    this.spyFn (stateName, delay, error);
                }
                brechen;
            } else {
                if (this.spyFn) {
                    this.spyFn (stateName, delay);
                }
            }
        }
        if (Fehler) {
            while (action = actions.shift ()) {
                action.unsubscribe ();
            }
            Fehler werfen;
        }
    }
    private detectStateName (Aktion: AsyncAction ): string {
        const c = Object.getPrototypeOf (action.state) .constructor;
        const argsPos = c.toString (). indexOf ('(');
        if (argsPos! == -1) {
            return c.toString (). substring (9, argsPos);
        }
        return null;
    }
}

Schauen wir uns zum Schluss die Verwendung an. Das Beispiel ist derselbe Unit-Test wie zuvor (löscht das vorherige Ergebnis), mit dem kleinen Unterschied, dass wir den Testplaner anstelle von fakeAsync / tick verwenden.

let testScheduler;
beforeEach (() => {
    testScheduler = new SpyingTestScheduler ();
    testScheduler.maxFrames = 1000000;
    monkeypatchScheduler (testScheduler);
    fixture.detectChanges ();
});
beforeEach (() => {
    spyOn (apiService, 'query'). and.callFake (() => {
        return Observable.of (queryResult) .delay (REQUEST_DELAY);
    });
});
it ('löscht das vorherige Ergebnis', (done: Function) => {
    comp.options = ['nicht leer'];
    testScheduler.onAction ((actionName: string, delay: number, err ?: any) => {
        if (actionName === 'DebounceTimeSubscriber' && delay === DEBOUNCING_VALUE) {
            expect (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);
            getan();
        }
    });
    SpecUtils.focusAndInput ('Londo', Fixture, 'Input');
    fixture.detectChanges ();
    testScheduler.flush ();
});

Der Testplaner wird im ersten beforeEach erstellt und mit einem Monkeypatch (!) Versehen. In der Sekunde vor JEDEM spionieren wir apiService.query aus, um das Ergebnis queryResult nach REQUEST_DELAY = 5000 Millisekunden zu liefern.

Lass uns nun das it Zeile für Zeile durchgehen:

  1. Beachten Sie zunächst, dass wir die Funktion done deklarieren, die wir in Verbindung mit dem Callback onAction des Testplaners benötigen. Das bedeutet, dass wir Jasmine mitteilen müssen, dass der Test selbst durchgeführt wird.
it ('löscht das vorherige Ergebnis', (done: Function) => {

2. Wieder geben wir einige Optionen vor, die in der Komponente vorhanden sind.

comp.options = ['nicht leer'];

3. Dies ist erklärungsbedürftig, da es auf den ersten Blick etwas unbeholfen erscheint. Wir möchten eine Aktion namens „DebounceTimeSubscriber“ mit einer Verzögerung von DEBOUNCING_VALUE = 300 Millisekunden abwarten. In diesem Fall möchten wir überprüfen, ob options.length den Wert 0 hat. Dann ist der Test abgeschlossen und wir rufen done () auf.

testScheduler.onAction ((actionName: string, delay: number, err ?: any) => {
    if (actionName === 'DebounceTimeSubscriber' && delay === DEBOUNCING_VALUE) {
      expect (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);
      getan();
    }
});

Sie sehen, dass die Verwendung von Test-Schedulern einige spezielle Kenntnisse über die internen Funktionen der Rxjs-Implementierung erfordert. Natürlich hängt es davon ab, welchen Test-Scheduler Sie verwenden, aber selbst wenn Sie einen leistungsfähigen Scheduler selbst implementieren, müssen Sie die Scheduler verstehen und einige Laufzeitwerte aus Gründen der Flexibilität verfügbar machen (was wiederum nicht unbedingt selbsterklärend sein kann).

4. Der Benutzer gibt erneut den Wert „Londo“ ein.

SpecUtils.focusAndInput ('Londo', Fixture, 'Input');

5. Erkennen Sie erneut Änderungen und rendern Sie die Vorlage erneut.

fixture.detectChanges ();

6. Schließlich führen wir alle Aktionen aus, die in der Warteschlange des Schedulers stehen.

testScheduler.flush ();

Zusammenfassung

Angulars eigene Testprogramme sind den selbst erstellten vorzuziehen ... solange sie funktionieren. In einigen Fällen funktioniert das fakeAsync / tick-Paar nicht, aber es gibt keinen Grund, verzweifelt auf die Unit-Tests zu verzichten. In diesen Fällen bietet sich ein Dienstprogramm zur automatischen Synchronisierung (hier auch als AsyncZoneTimeInSyncKeeper bezeichnet) oder ein benutzerdefinierter Testplaner (hier auch als SpyingTestScheduler bezeichnet) an.

Quellcode