web-dev-qa-db-fra.com

Comment tester la saga redux avec jest?

Juste nouveau dans réagir, réagir-redux/saga et plaisanterie

considérer:

----- Le compagnon () ----

componentDidMount() {

    this.props.actions.initTodos(
        axios,
        ajaxURLConstants.WP_GET_TODOS,
        appStateActions.setAppInIdle,
        appStateActions.setAppInProcessing,
        todosActions.todosInitialized
    );

}

Ainsi, lorsque mon composant TodoApp a été monté, il envoie l'action INIT_TODOS que mon saga racine écoute et, lorsqu'il le détecte, génère la saga appropriée des travailleurs pour agir en conséquence.

----- La saga des travailleurs correspondants -----

export function* initTodosSaga( action ) {

    try {

        yield put( action.setAppInProcessing() );

        let response = yield call( action.axios.get , action.WP_GET_TODOS );

        if ( response.data.status === "success" )
            yield put( action.todosInitialized( response.data.todos ) );
        else {

            console.log( response );
            alert( response.data.error_msg );

        }

    } catch ( error ) {

        console.log( "error" , error );
        alert( "Failed to load initial data" );            

    }

    yield put( action.setAppInIdle() );

}

----- Le test jusqu'à présent -----

import todos             from "../../__fixtures__/todos";
import { initTodosSaga } from "../todosSaga";

test( "saga test" , () => {

    let response = {
            status : "success",
            todos
        },
        action = {
            axios : {
                get : function() {

                    return new Promise( ( resolve , reject ) => {

                        resolve( response );

                    } );

                }
            },
            WP_GET_TODOS       : "dummy url",
            setAppInIdle       : jest.fn(),
            setAppInProcessing : jest.fn(),
            todosInitialized   : jest.fn()
        };

    let initTodosSagaGen = initTodosSaga( action );

    initTodosSagaGen.next();

    expect( action.setAppInIdle ).toHaveBeenCalled();

} );

----- Le résultat du test -----

 enter image description here

Donc, la partie importante est cette 

console.error_modules_noeud\redux-saga\lib\internal\utils.js: 240

uncaught at check put (action): l'action argument n'est pas définie

mais j'ai console.log l'action que j'ai transmise lors de mon test dans la saga des travailleurs et, en effet, ce n'est pas indéfini

qu'est-ce que je rate?

Merci d'avance.

----------Mettre à jour------------

Ok remarquez en haut qu'il se plaint de cette ligne de code

yield put( action.setAppInIdle() );

Ce qui est en dehors du bloc try catch, donc j'ai fait quelques changements

1.) J'ai déplacé le code ci-dessus à l'intérieur du bloc try catch, juste après la déclaration else de 

if ( response.data.status === "success" )

veuillez vérifier le code initTodosSaga ci-dessus

Puis sur mon test de saga, je teste pour 

expect( action.setAppInProcessing ).toHaveBeenCalled();

au lieu de la fonction d'espionnage setAppInIdle

et c'est le résultat du test

 enter image description here

donc le test a réussi! mais il se plaint toujours que l'action soit indéfinie

maintenant ce qui est intéressant est si dans mon test de saga, si je teste pour cela maintenant

expect( action.setAppInProcessing ).toHaveBeenCalled();
expect( action.setAppInIdle ).toHaveBeenCalled();

C'est le résultat

 enter image description here

alors maintenant, il se plaint toujours de l'action toujours indéfinie (je n'ai pas inclus dans ma capture d'écran, mais toujours comme ci-dessus)

plus la deuxième affirmation que j’ai à propos de la fonction d’espionnage setAppInIdle n’a pas été appelée, mais le setAppInProcessing a été adopté!

J'espère que cette information supplémentaire aide à résoudre cette question.

6
Jplus2

Il semble très difficile de tester la saga Redux sans l'aide d'une bibliothèque externe.

Pour moi, j'ai utilisé https://github.com/jfairbank/redux-saga-test-plan

Cette bibliothèque est très bonne.

Alors voici mes tests maintenant

(--------------------- Test 1 ---------------------} _

Donc, pour ce test, j’ai transmis à la charge d’action presque tout ce dont la saga a besoin pour fonctionner, ex. axios, fonctions de créateur d’action, etc ............. ressemble plus à suivre le principe de l’injection de dépendance, il est donc facile à tester.

----- Composant TodoApp -----

componentDidMount() {

    this.props.actions.initTodos(
        axios,
        ajaxURLConstants.WP_GET_TODOS,
        appStateActions.setAppInIdle,
        appStateActions.setAppInProcessing,
        todosActions.todosInitialized,
        todosActions.todosFailedInit
    );

}

Ainsi, lorsque le composant est monté, il déclenche une action que ma saga racine écoute et capture, puis génère la saga appropriée des travailleurs pour agir en conséquence.

remarquez encore une fois que je transmets toutes les données nécessaires dont la saga des travailleurs aurait besoin pour fonctionner correctement dans la charge utile des actions.

_ {----- initTodoSaga (Worker Saga) ----- _

export function* initTodosSaga( action ) {

    try {

        yield put( action.setAppInProcessing() );

        let response = yield call( action.axios.get , action.WP_GET_TODOS );

        if ( response.data.status === "success" )
            yield put( action.todosInitialized( response.data.todos ) );
        else {

            console.log( response );
            alert( response.data.error_msg );

            yield put( action.todosFailedInit( response ) );

        }

    } catch ( error ) {

        console.log( "error" , error );
        alert( "Failed to load initial data" );

        yield put( action.todosFailedInit( error ) );

    }

    yield put( action.setAppInIdle() );

}

_ {----- ---- test de saga -----

import { expectSaga }    from "redux-saga-test-plan";
import { initTodosSaga } from "../todosSaga";

test( "should initialize the ToDos state via the initTodoSaga" , () => {

    let response = {

            data : {
                status : "success",
                todos
            }

        },
        action = {
            axios : {
                get : function() {

                    return new Promise( ( resolve , reject ) => {

                        resolve( response );

                    } );

                }
            },
            WP_GET_TODOS       : "dummy url",
            setAppInIdle       : appStateActions.setAppInIdle,
            setAppInProcessing : appStateActions.setAppInProcessing,
            todosInitialized   : todosStateActions.todosInitialized,
            todosFailedInit    : todosStateActions.todosFailedInit
        };

    // This is the important bit
    // These are the assertions
    // Basically saying that the actions below inside the put should be dispatched when this saga is executed
    return expectSaga( initTodosSaga , action )
        .put( appStateActions.setAppInProcessing() )
        .put( todosStateActions.todosInitialized( todos ) )
        .put( appStateActions.setAppInIdle() )
        .run();

} );

et mon test passe yay! :) maintenant pour vous montrer le message d'erreur lorsqu'un test échoue, je commenterai cette ligne de code dans mon initTodosSaga

yield put( action.setAppInIdle() );

alors maintenant l'affirmation 

.put( appStateActions.setAppInIdle() )

devrait échouer maintenant

 enter image description here

donc il sort (attente non satisfaite} _ ce qui est logique car l'action que nous nous attendions à être renvoyée n'a pas

(--------------------- Test 2 --------------------)

Maintenant, ce test concerne une saga dans laquelle il importe certaines choses dont il a besoin pour fonctionner, contrairement à mon premier test, où je nourris axios, créateurs d’action dans la charge utile

Cette saga a importé axios, des créateurs d’action nécessaires

Heureusement, _ {plan de test de la saga Redux} _ a quelques fonctions d'assistance pour "alimenter" données factices dans la saga

Je vais simplement ignorer le composant qui déclenche l'action que la saga root écoute, ce n'est pas important, je vais simplement coller directement la saga et le test de la saga

---- addTodoSaga ----

/** global ajaxurl */
import axios                from "axios";
import { call , put }       from "redux-saga/effects";
import * as appStateActions from "../actions/appStateActions";
import * as todosActions    from "../actions/todosActions";

export function* addTodoSaga( action ) {

    try {

        yield put( appStateActions.setAppInProcessing() );

        let formData = new FormData;

        formData.append( "todo" , JSON.stringify( action.todo ) );

        let response = yield call( axios.post , ajaxurl + "?action=wptd_add_todo" , formData );

        if ( response.data.status === "success" ) {

            yield put( todosActions.todoAdded( action.todo ) );
            action.successCallback();

        } else {

            console.log( response );
            alert( response.data.error_msg );

        }

    } catch ( error ) {

        console.log( error );
        alert( "Failed to add new todo" );

    }

    yield put( appStateActions.setAppInIdle() );

}

-----Le test-----

import axios          from "axios";
import { expectSaga } from "redux-saga-test-plan";
import * as matchers  from "redux-saga-test-plan/matchers";
import * as appStateActions   from "../../actions/appStateActions";
import * as todosStateActions from "../../actions/todosActions";
import { addTodoSaga } from "../todosSaga";

test( "should dispatch TODO_ADDED action when adding new todo is successful" , () => {

   let response = {
            data : { status : "success" }
        },
        todo = {
            id        : 1,
            completed : false,
            title     : "Browse 9gag tonight"
        },
        action = {
            todo,
            successCallback : jest.fn()
        };

    // Here are the assertions
    return expectSaga( addTodoSaga , action )
        .provide( [
            [ matchers.call.fn( axios.post ) , response ]
        ] )
        .put( appStateActions.setAppInProcessing() )
        .put( todosStateActions.todoAdded( todo ) )
        .put( appStateActions.setAppInIdle() )
        .run();

} );

Ainsi, la fonction supply vous permet de simuler un appel de fonction tout en fournissant des données factices qu’il doit renvoyer.

et voilà, je peux maintenant tester mes sagas! Yay!

une dernière chose, quand je lance un test pour ma saga qui aboutit à l'exécution d'un code avec un code d'alerte

ex.

alert( "Earth is not flat!" );

Je l'ai sur la console 

Error: Not implemented: window.alert

et un tas de traces de pile en dessous, alors peut-être que c'est parce que l'objet d'alerte n'est pas présent sur le noeud? Comment puis-je cacher cela? ajoutez simplement le commentaire si vous avez une réponse.

J'espère que cela aide quelqu'un

1
Jplus2

Voici une version de travail de votre test:

import todos from '../../__fixtures__/todos';
import { initTodosSaga } from '../todosSaga';
import { put, call } from 'redux-saga/effects';

test('saga test', () => {
    const response = {
        data: {
            status: 'success',
            todos
        }
    };
    const action = {
        axios: {
            get() {}
        },
        WP_GET_TODOS: 'dummy url',
        setAppInIdle: jest.fn().mockReturnValue({ type: 'setAppInIdle' }),
        setAppInProcessing: jest.fn().mockReturnValue({ type: 'setAppInProcessing' }),
        todosInitialized: jest.fn().mockReturnValue({ type: 'todosInitialized' })
    };
    let result;

    const initTodosSagaGen = initTodosSaga(action);

    result = initTodosSagaGen.next();
    expect(result.value).toEqual(put(action.setAppInProcessing()));
    expect(action.setAppInProcessing).toHaveBeenCalled();
    result = initTodosSagaGen.next();
    expect(result.value).toEqual(call(action.axios.get, action.WP_GET_TODOS));
    result = initTodosSagaGen.next(response);
    expect(action.todosInitialized).toHaveBeenCalled();
    expect(result.value).toEqual(put(action.todosInitialized(response.data.todos)));
    result = initTodosSagaGen.next();
    expect(action.setAppInIdle).toHaveBeenCalled();
    expect(result.value).toEqual(put(action.setAppInIdle()));
});

Quelques notes:

  • Vous n'êtes pas obligé de laisser le faux Axios.get renvoyer quoi que ce soit
  • Avec les instructions expect, je compare le rendement du générateur à ce que le générateur doit faire (c'est-à-dire exécuter les instructions put et call)
  • La propriété data était absente de votre réponse fictive
1
Patrick Hund