Metamorphose beim Testen der Redux-Saga

Das redux-saga Framework bietet eine Reihe interessanter Muster für die Arbeit mit Nebenwirkungen, aber wie echte blutige Unternehmensentwickler müssen wir unseren gesamten Code mit Tests abdecken. Lassen Sie uns herausfinden, wie wir unsere Sagen testen werden.



Nehmen Sie als Beispiel den einfachsten Clicker. Der Datenfluss und die Bedeutung der Anwendung sind wie folgt:

  1. Der Benutzer drückt eine Taste.
  2. Eine Anfrage wird an den Server gesendet und informiert, dass der Benutzer eine Schaltfläche gedrückt hat.
  3. Der Server gibt die Anzahl der Klicks zurück.
  4. Der Status zeichnet die Anzahl der Klicks auf.
  5. Die Benutzeroberfläche wird aktualisiert und der Benutzer sieht, dass die Anzahl der Klicks gestiegen ist.
  6. ...
  7. GEWINN.

In unserer Arbeit verwenden wir Typescript, sodass alle Beispiele in dieser Sprache vorliegen.

Wie Sie wahrscheinlich schon vermutet haben, werden wir dies alles mit redux-saga umsetzen. Hier ist der Code für die gesamte Sagendatei:

 export function* processClick() { const result = yield call(ServerApi.SendClick) yield put(Actions.clickSuccess(result)) } export function* watchClick() { yield takeEvery(ActionTypes.CLICK, processClick) } 

In diesem einfachen Beispiel deklarieren wir die Saga watchClick , die die Aktion direkt verarbeitet, und die Saga watchClick , die eine Schleife für die Verarbeitung von action' .

Generatoren


Wir haben also die einfachste Saga. Es sendet eine Anfrage an den Server ( call) , empfängt das Ergebnis und leitet es an den Reduzierer weiter ( put) . Wir müssen irgendwie testen, ob die Saga genau das überträgt, was sie vom Server empfängt. Fangen wir an.

Zum Testen müssen wir den Serveraufruf sperren und irgendwie prüfen, ob genau das, was vom Server kam, in den Reduzierer gelangt ist.

Da Sagen Generatorfunktionen sind, ist die next() -Methode, die sich im Generatorprototyp befindet, die naheliegendste Methode zum Testen. Bei Verwendung dieser Methode haben wir die Möglichkeit, sowohl den nächsten Wert vom Generator zu empfangen als auch den Wert an den Generator zu übertragen. So haben wir sofort die Möglichkeit, nasse Anrufe zu erhalten. Aber ist alles so rosig? Hier ist ein Test, den ich für nackte Generatoren geschrieben habe:

 it('should increment click counter (behaviour test)', () => { const saga = processClick() expect(saga.next().value).toEqual(call(ServerApi.SendClick)) expect(saga.next(10).value).toEqual(put(Actions.clickSuccess(10))) }) 

Der Test war kurz, aber was testet er? Tatsächlich wiederholt es einfach den Code der Saga-Methode, dh bei jeder Änderung der Saga muss der Test geändert werden.

Ein solcher Test hilft bei der Entwicklung nicht.

Redux-Saga-Test-Plan


Nachdem wir auf dieses Problem gestoßen waren, entschieden wir uns, es zu googeln und stellten plötzlich fest, dass wir nicht die einzigen waren und weit vom ersten entfernt. Direkt in der Dokumentation zur redux-saga bieten Entwickler einen Blick auf mehrere Bibliotheken, die speziell für redux-saga erstellt wurden.

Aus der vorgeschlagenen Liste haben wir die redux-saga-test-plan Bibliothek redux-saga-test-plan . Hier ist der Code für die erste Version des Tests, den ich damit geschrieben habe:

 it('should increment click counter (behaviour test with test-plan)', () => { return expectSaga(processClick) .provide([ call(ServerApi.SendClick), 2] ]) .dispatch(Actions.click()) .call(ServerApi.SendClick) .put(Actions.clickSuccess(2)) .run() }) 

Der Testkonstruktor in redux-saga-test-plan ist die expectSaga Funktion, die die Schnittstelle zurückgibt, die den Test beschreibt. Die Testsaga ( processClick aus der ersten Liste) wird an die Funktion selbst übergeben.

Mit der provide können Sie Serveraufrufe oder andere Abhängigkeiten blockieren. Es wird ein Array von StaticProvider' , die beschreiben, welche Methode zurückgegeben werden soll.

Im Act Block haben wir eine einzige Methode - den dispatch . Es wird eine Aktion übergeben, auf die die Saga reagiert.

Der assert Block besteht aus den call put Methoden, die prüfen, ob die entsprechenden Effekte während der Arbeit der Saga verursacht wurden.

Alles endet mit der run() -Methode. Diese Methode führt den Test direkt aus.

Die Vorteile dieses Ansatzes:

  • Es wird überprüft, ob die Methode aufgerufen wurde und nicht die Reihenfolge der Aufrufe.
  • moki beschreibt klar, welche Funktion nass wird und was zurückkehrt.

Es gibt jedoch noch viel zu tun:

  • es gibt mehr Code;
  • Der Test ist schwer zu lesen.
  • Dies ist ein Verhaltenstest, was bedeutet, dass er immer noch mit der Umsetzung der Saga verbunden ist.

Die letzten beiden Schläge


Zustandstest


Zuerst beheben wir den letzten: Wir machen einen Zustandstest aus einem Verhaltenstest. Die Tatsache, dass Sie mit dem Testplan den Anfangszustand festlegen und den reducer passieren können, der auf die durch die Saga erzeugten put Effekte reagieren soll, hilft uns dabei. Es sieht so aus:

 it('should increment click counter (state test with test-plan)', () => { const initialState = { clickCount: 11, return expectSaga(processClick) .provide([ call(ServerApi.SendClick), 14] ]) .withReducer(rootReducer, initialState) .dispatch(Actions.click()) .run() .then(result => expect(result.storeState.clickCount).toBe(14)) }) 

In diesem Test überprüfen wir nicht mehr, ob Effekte ausgelöst wurden. Wir überprüfen den endgültigen Zustand nach der Ausführung, und das ist in Ordnung.

Wir haben es geschafft, die Implementierung der Saga loszuwerden. Versuchen wir nun, den Test verständlicher zu machen. Dies ist einfach, wenn Sie then() durch async/await ersetzen:

 it('should increment click counter (state test with test-plan async-way)', async () => { const initialState = { clickCount: 11, } const saga = expectSaga(processClick) .provide([ call(ServerApi.SendClick), 14] ]) .withReducer(rootReducer, initialState) const result = await saga.dispatch(Actions.click()).run() expect(result.storeState.clickCount).toBe(14) }) 

Integrationstests


Aber was ist, wenn wir auch eine Reverse-Click-Operation haben (nennen wir es Unclick) und unsere Sag-Datei jetzt so aussieht:

 export function* processClick() { const result = yield call(ServerApi.SendClick) yield put(Actions.clickSuccess(result)) } export function* processUnclick() { const result = yield call(ServerApi.SendUnclick) yield put(Actions.clickSuccess(result)) } function* watchClick() { yield takeEvery(ActionTypes.CLICK, processClick) } function* watchUnclick() { yield takeEvery(ActionTypes.UNCLICK, processUnclick) } export default function* mainSaga() { yield all([watchClick(), watchUnclick()]) } 

Angenommen, wir müssen testen, ob das Ergebnis der letzten Fahrt zum Server in den Status geschrieben wird, wenn die Aktionen zum Klicken und Entfernen im Status aufgerufen werden. Ein solcher Test kann auch leicht mit dem redux-saga-test-plan :

 it('should change click counter (integration test)', async () => { const initialState = { clickCount: 11, } const saga = expectSaga(mainSaga) .provide([ call(ServerApi.SendClick), 14], call(ServerApi.SendUnclick), 18] ]) .withReducer(rootReducer, initialState) const result = await saga .dispatch(Actions.click()) .dispatch(Actions.unclick()) .run() expect(result.storeState.clickCount).toBe(18) }) 

Bitte beachten Sie, dass wir jetzt mainSaga testen und nicht einzelne mainSaga Handler.

Wenn wir diesen Test jedoch so ausführen, wie er ist, erhalten wir das Vorning:



Dies ist auf den takeEvery Effekt zurückzuführen. Dies ist eine Nachrichtenverarbeitungsschleife, die funktioniert, solange unsere Anwendung geöffnet ist. Dementsprechend kann der Test, in dem takeEvery aufgerufen takeEvery , die Arbeit nicht ohne fremde Hilfe abschließen, und der redux-saga-test-plan takeEvery beendet diese Effekte nach 250 ms nach Beginn des Tests zwangsweise. Dieses Zeitlimit kann durch Aufrufen von expectedSaga.DEFAULT_TIMEOUT = 50 geändert werden.
Wenn Sie solche Vorings nicht erhalten möchten, einen für jeden Test mit einem komplexen Effekt, verwenden Sie einfach die silentRun() -Methode anstelle der run() -Methode.



Fallstricke


Wo ohne Fallstricke ... Zum Zeitpunkt dieses Schreibens war die neueste Version der Redux-Saga: 1.0.2. Gleichzeitig kann redux-saga-test-plan nur mit JS damit arbeiten.

Wenn Sie TypeScript möchten, müssen Sie die Version vom Beta-Kanal installieren:
npm install redux-saga-test-plan@beta
und deaktivieren Sie Tests aus dem Build. Dazu müssen Sie in der Datei tsconfig.json den Pfad "./src/**/*.spec.ts" im Feld "ausschließen" angeben.

Trotzdem halten wir den redux-saga-test-plan redux-saga für die beste Bibliothek zum Testen der redux-saga . Wenn Sie redux-saga in Ihrem Projekt haben, ist es vielleicht eine gute Wahl für Sie.

Der Quellcode des Beispiels auf GitHub .

Source: https://habr.com/ru/post/de458500/


All Articles