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:
- Der Benutzer drückt eine Taste.
- Eine Anfrage wird an den Server gesendet und informiert, dass der Benutzer eine Schaltfläche gedrückt hat.
- Der Server gibt die Anzahl der Klicks zurück.
- Der Status zeichnet die Anzahl der Klicks auf.
- Die Benutzeroberfläche wird aktualisiert und der Benutzer sieht, dass die Anzahl der Klicks gestiegen ist.
- ...
- 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 .