web-dev-qa-db-fra.com

Test des actions envoyées dans Redux Thunk avec Jest

Je suis assez nouveau pour Jest et je ne suis certes pas un expert en test de code async ... 

J'ai un simple assistant Fetch que j'utilise:

export function fetchHelper(url, opts) {
    return fetch(url, options)
        .then((response) => {
            if (response.ok) {
                return Promise.resolve(response);
            }

            const error = new Error(response.statusText || response.status);
            error.response = response;

            return Promise.reject(error);
        });
    }

Et implémentez-le comme ceci:

export function getSomeData() {
    return (dispatch) => {
        return fetchHelper('http://datasource.com/').then((res) => {
            dispatch(setLoading(true));
            return res.json();
        }).then((data) => {
            dispatch(setData(data));
            dispatch(setLoading(false));
        }).catch(() => {
            dispatch(setFail());
            dispatch(setLoading(false));
        });
    };
}

Cependant, je veux vérifier que les bons messages sont envoyés dans les bonnes circonstances et dans le bon ordre.

Auparavant, cela était assez facile avec une sinon.spy(), mais je n'arrive pas à comprendre comment le reproduire dans Jest. Idéalement, j'aimerais que mon test ressemble à ceci:

expect(spy.args[0][0]).toBe({
  type: SET_LOADING_STATE,
  value: true,
});


expect(spy.args[1][0]).toBe({
  type: SET_DATA,
  value: {...},
});

Merci d'avance pour toute aide ou conseil!

7
DanV

Les redux docs ont un excellent article sur le test des créateurs d’actions asynchrones :

Pour les créateurs d'actions asynchrones utilisant Redux Thunk ou un autre middleware, il est préférable de se moquer complètement du magasin Redux pour les tests. Vous pouvez appliquer le middleware à un magasin factice à l'aide de redux-mock-store . Vous pouvez également utiliser fetch-mock pour simuler les requêtes HTTP.

import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import * as actions from '../../actions/TodoActions'
import * as types from '../../constants/ActionTypes'
import fetchMock from 'fetch-mock'
import expect from 'expect' // You can use any testing library

const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)

describe('async actions', () => {
  afterEach(() => {
    fetchMock.reset()
    fetchMock.restore()
  })

  it('creates FETCH_TODOS_SUCCESS when fetching todos has been done', () => {
    fetchMock
      .getOnce('/todos', { body: { todos: ['do something'] }, headers: { 'content-type': 'application/json' } })


    const expectedActions = [
      { type: types.FETCH_TODOS_REQUEST },
      { type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something'] } }
    ]
    const store = mockStore({ todos: [] })

    return store.dispatch(actions.fetchTodos()).then(() => {
      // return of async actions
      expect(store.getActions()).toEqual(expectedActions)
    })
  })
})

Leur approche n’est pas d’utiliser jest (ou sinon) pour espionner, mais d’utiliser un magasin factice et d’affirmer les actions expédiées. Cela a l'avantage de pouvoir gérer les thunks qui envoient des thunks, ce qui peut être très difficile à faire avec des espions.

Tout cela vient directement de la documentation, mais laissez-moi savoir si vous voulez que je crée un exemple pour votre thunk.

6
Michael Peyper

Pour les créateurs d'actions asynchrones utilisant Redux Thunk ou un autre middleware, il est préférable de se moquer complètement du magasin Redux pour les tests. Vous pouvez appliquer le middleware à un magasin factice à l'aide de redux-mock-store. Afin de simuler la requête HTTP, vous pouvez utiliser nock

Selon redux-mock-store documentation , vous devrez appeler store.getActions() à la fin de la demande pour tester les actions asynchrones. Vous pouvez configurer votre test de la manière suivante:

mockStore(getState?: Object,Function) => store: Function Retourne un instance du magasin fictif configuré. Si vous souhaitez réinitialiser votre magasin après chaque test, vous devez appeler cette fonction.

store.dispatch(action) => action Envoie une action à travers le magasin simulé. L'action sera stockée dans un tableau à l'intérieur de l'instance et exécuté.

store.getState() => state: Object Renvoie l'état de la maquette le magasin

store.getActions() => actions: Array Renvoie les actions de la maquette le magasin

store.clearActions() Efface les actions stockées

Vous pouvez écrire l'action de test comme

import nock from 'nock';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';

//Configuring a mockStore
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

//Import your actions here
import {setLoading, setData, setFail} from '/path/to/actions';

test('test getSomeData', () => {
    const store = mockStore({});

    nock('http://datasource.com/', {
       reqheaders // you can optionally pass the headers here
    }).reply(200, yourMockResponseHere);

    const expectedActions = [
        setLoading(true),
        setData(yourMockResponseHere),
        setLoading(false)
    ];

    const dispatchedStore = store.dispatch(
        getSomeData()
    );
    return dispatchedStore.then(() => {
        expect(store.getActions()).toEqual(expectedActions);
    });
});

P.S. N'oubliez pas que le magasin factice ne se met pas à jour lorsque l'action fictive est déclenchée et si vous comptez sur les données mises à jour après l'action précédente à utiliser dans l'action suivante, vous devez écrire votre propre instance de il aime

const getMockStore = (actions) => {
    //action returns the sequence of actions fired and 
    // hence you can return the store values based the action
    if(typeof action[0] === 'undefined') {
         return {
             reducer: {isLoading: true}
         }
    } else {
        // loop over the actions here and implement what you need just like reducer

    }
}

puis configurez la mockStore comme

 const store = mockStore(getMockStore);

J'espère que ça aide. Vérifiez également this dans la documentation redux sur le test des créateurs d’actions asynchrones 

5
Shubham Khatri

Si vous vous moquez de la fonction de répartition avec jest.fn(), vous pouvez simplement accéder à dispatch.mock.calls pour recevoir tous les appels passés sur votre stub.

  const dispatch = jest.fn();
  actions.yourAction()(dispatch);

  expect(dispatch.mock.calls.length).toBe(1);

  expect(dispatch.mock.calls[0]).toBe({
    type: SET_DATA,
    value: {...},
  });
1
Canastro

Dans ma réponse, j’utilise axios au lieu de fetch car je n’ai pas beaucoup d’expérience dans la recherche de promesses, cela n’a aucune importance pour votre question. Personnellement, je me sens très à l'aise avec axios
Regardez l'exemple de code que je fournis ci-dessous:

// apiCalls.js
const fetchHelper = (url) => {
  return axios.get(url);
}


import * as apiCalls from './apiCalls'
describe('getSomeData', () => {
  it('should dispatch SET_LOADING_STATE on start of call', async () => {
    spyOn(apiCalls, 'fetchHelper').and.returnValue(Promise.resolve());
    const mockDispatch = jest.fn();

    await getSomeData()(mockDispatch);

    expect(mockDispatch).toHaveBeenCalledWith({
      type: SET_LOADING_STATE,
      value: true,
    });
  });

  it('should dispatch SET_DATA action on successful api call', async () => {
    spyOn(apiCalls, 'fetchHelper').and.returnValue(Promise.resolve());
    const mockDispatch = jest.fn();

    await getSomeData()(mockDispatch);

    expect(mockDispatch).toHaveBeenCalledWith({
      type: SET_DATA,
      value: { ...},
    });
  });

  it('should dispatch SET_FAIL action on failed api call', async () => {
    spyOn(apiCalls, 'fetchHelper').and.returnValue(Promise.reject());
    const mockDispatch = jest.fn();

    await getSomeData()(mockDispatch);

    expect(mockDispatch).toHaveBeenCalledWith({
      type: SET_FAIL,
    });
  });
});

Ici, je me moque de l’aide à la récupération pour renvoyer la promesse résolue de tester le succès et de rejeter la promesse de tester l’appel échoué de l’API. Vous pouvez également leur transmettre des arguments pour valider la réponse.
Vous pouvez implémenter getSomeData comme ceci: 

const getSomeData = () => {
  return (dispatch) => {
    dispatch(setLoading(true));
    return fetchHelper('http://datasource.com/')
      .then(response => {
        dispatch(setData(response.data));
        dispatch(setLoading(false));
      })
      .catch(error => {
        dispatch(setFail());
        dispatch(setLoading(false));
      })
  }
}

J'espère que cela résoudra votre problème. Veuillez commenter, si vous avez besoin d'éclaircissements.
P.S Vous pouvez voir en regardant dans le code ci-dessus pourquoi je préfère axios que chercher, cela vous évite beaucoup de promesses résolues! 
Pour en savoir plus à ce sujet, vous pouvez consulter: https://medium.com/@thejasonfile/fetch-vs-axios-js-for-making-http-requests-2b261cdd3af5

0
Swapnil