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!
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.
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
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: {...},
});
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