Métamorphose des tests de redux-saga

Le cadre redux-saga fournit un tas de modèles intéressants pour travailler avec les effets secondaires, mais, comme les vrais développeurs d'entreprise sanglants, nous devons couvrir l'ensemble de notre code avec des tests. Voyons comment nous allons tester nos sagas.



Prenons l'exemple du clicker le plus simple. Le flux de données et la signification de l'application seront les suivants:

  1. L'utilisateur enfonce un bouton.
  2. Une demande est envoyée au serveur, informant que l'utilisateur a piqué un bouton.
  3. Le serveur renvoie le nombre de clics effectués.
  4. L'état enregistre le nombre de clics effectués.
  5. L'interface utilisateur est mise à jour et l'utilisateur voit que le nombre de clics a augmenté.
  6. ...
  7. PROFIT.

Dans notre travail, nous utilisons Typescript, donc tous les exemples seront dans cette langue.

Comme vous l'avez probablement déjà deviné, nous implémenterons tout cela avec redux-saga . Voici le code de l'ensemble du fichier sagas:

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

Dans cet exemple simple, nous déclarons la saga processClick , qui traite directement l'action et la saga watchClick , qui crée une boucle pour le traitement des action' .

Générateurs


Nous avons donc la saga la plus simple. Il envoie une requête au serveur ( call) , reçoit le résultat et le transmet au réducteur ( put) . Nous devons en quelque sorte tester si la saga transmet exactement ce qu'elle reçoit du serveur. Commençons.

Pour les tests, nous devons verrouiller l'appel du serveur et vérifier en quelque sorte si exactement ce qui provenait du serveur est entré dans le réducteur.

Étant donné que les sagas sont des fonctions de générateur, la méthode next() , qui se trouve dans le prototype de générateur, sera le moyen le plus évident de tester. Lorsque vous utilisez cette méthode, nous avons à la fois la possibilité de recevoir la prochaine valeur du générateur et de transférer la valeur au générateur. Ainsi, nous sortons de la boîte la possibilité de recevoir des appels mouillés. Mais tout est-il si rose? Voici un test que j'ai écrit sur des générateurs nus:

 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))) }) 

Le test était concis, mais que teste-t-il? En fait, il répète simplement le code de la méthode de la saga, c'est-à-dire qu'avec tout changement dans la saga, le test devra être changé.

Un tel test n'aide pas au développement.

Plan de test de Redux-saga


Après avoir rencontré ce problème, nous avons décidé de le rechercher sur Google et avons soudainement réalisé que nous n'étions pas les seuls et loin du premier. Directement dans la documentation de redux-saga développeurs proposent un aperçu de plusieurs bibliothèques créées spécifiquement pour satisfaire les fans de tests.

De la liste proposée, nous avons pris la bibliothèque redux-saga-test-plan . Voici le code de la première version du test que j'ai écrit avec:

 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() }) 

Le constructeur de test dans redux-saga-test-plan est la fonction expectSaga , qui renvoie l'interface qui décrit le test. La saga de test ( processClick de la première liste) est passée à la fonction elle-même.

À l'aide de la méthode provide , vous pouvez bloquer les appels de serveur ou d'autres dépendances. Un tableau de StaticProvider' , qui décrit quelle méthode doit retourner.

Dans le bloc Act , nous avons une seule méthode - l' dispatch . Une action lui est transmise, à laquelle la saga va répondre.

Le bloc assert compose des méthodes call put , qui vérifient si les effets correspondants ont été causés pendant le travail de la saga.

Tout se termine avec la méthode run() . Cette méthode exécute le test directement.

Les avantages de cette approche:

  • Il vérifie si la méthode a été appelée et non la séquence d'appels;
  • moki décrit clairement quelle fonction est mouillée et ce qui revient.

Cependant, il y a du travail à faire:

  • il y a plus de code;
  • le test est difficile à lire;
  • il s'agit d'un test de comportement, ce qui signifie qu'il est toujours lié à la mise en œuvre de la saga.

Les deux derniers coups


Test de condition


Tout d'abord, nous corrigeons le dernier: nous faisons un test d'état à partir d'un test de comportement. Le fait que test-plan vous permette de définir l' state initial et de passer le reducer , qui devrait répondre aux effets de put générés par la saga, nous y aidera. Cela ressemble à ceci:

 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)) }) 

Dans ce test, nous ne vérifions plus que des effets ont été déclenchés. Nous vérifions l'état final après l'exécution, et ça va.

Nous avons réussi à nous débarrasser de l'implémentation de la saga, essayons maintenant de rendre le test plus compréhensible. C'est facile si vous remplacez then() par async/await :

 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) }) 

Tests d'intégration


Mais que se passe-t-il si nous obtenons également une opération de clic inversé (appelons-la unclick), et maintenant notre fichier sag ressemble à ceci:

 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()]) } 

Supposons que nous devions tester que lorsque les actions de clic et de déclic sont appelées dans l'état, le résultat du dernier trajet vers le serveur est écrit dans l'état. Un tel test peut également être facilement effectué avec 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) }) 

Veuillez noter que nous testons mainSaga , et non des gestionnaires d' mainSaga individuels.

Cependant, si nous exécutons ce test tel quel, nous obtiendrons le Vorning:



Cela est dû à l'effet takeEvery - il s'agit d'une boucle de traitement des messages qui fonctionnera pendant que notre application est ouverte. En conséquence, le test dans lequel takeEvery est appelé ne pourra pas terminer le travail sans aide extérieure, et redux-saga-test-plan met fin de force à ces effets après 250 ms après le début du test. Ce délai peut être modifié en appelant expectSaga.DEFAULT_TIMEOUT = 50.
Si vous ne souhaitez pas recevoir de tels vorings, un pour chaque test avec un effet complexe, utilisez simplement la méthode silentRun() au lieu de la méthode run() .



Pièges


Où sans écueils ... Au moment d'écrire ces lignes, la dernière version de redux-saga: 1.0.2. Dans le même temps, redux-saga-test-plan ne peut fonctionner qu'avec JS.

Si vous voulez TypeScript, vous devez installer la version à partir du canal bêta:
npm install redux-saga-test-plan@beta
et désactivez les tests de la version. Pour ce faire, dans le fichier tsconfig.json, vous devez spécifier le chemin "./src/**/*.spec.ts" dans le champ "exclure".

Malgré cela, nous considérons que redux-saga-test-plan la meilleure bibliothèque pour tester redux-saga . Si vous avez redux-saga dans votre projet, ce sera peut-être un bon choix pour vous.

Le code source de l'exemple sur GitHub .

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


All Articles