J'essaie de tester tous les scénarios que ma saga pourrait suivre, mais je n'arrive pas à faire se produire les comportements que je veux. C'est assez simple, j'ai une requête HTTP (login), et je veux tester le succès et les cas d'échec en se moquant de ma méthode API.
Mais, il ressemble au call effect
ne déclenche pas ma fonction api, je ne sais pas encore vraiment comment cela fonctionne, mais je suppose que le middleware est en charge d'invoquer la fonction, et comme je ne passe pas par le magasin sur mon test, je ne peut pas obtenir le résultat.
Donc, ma question est, comment pouvez-vous tester votre saga lorsque vous devez envoyer différentes actions (généralement un succès ou un échec) à côté de votre appel asynchrone?
J'ai cherché un exemple, j'ai trouvé des sagas avec succès et échec mais le cas d'échec n'est jamais testé, par exemple dans l'exemple du panier ici
SAGA.JS
export function* login(action) {
try {
const user = yield call(api.login, action);
return yield put(actions.loginSuccess(user));
} catch(e) {
yield put(actions.loginFail(e));
}
}
export default function* rootAuthenticationSagas() {
yield* takeLatest(LOGIN, login);
}
TEST.JS
describe('login', () => {
context('When it fails', () => {
before('Stub the api', () => {
sinon.stub(api, 'login', () => {
// IT NEVER COMES HERE !
return Promise.reject({ error: 'user not found' });
});
});
it('should return a LOGIN_FAIL action', () => {
const action = {
payload: {
name: 'toto',
password: '123456'
}
};
const generator = login(action);
// THE CALL YIELD
generator.next();
const expectedResult = put({ type: 'LOGIN_FAIL', payload: { error: 'user not found' } });
expect(generator.next().value).to.be.eql(expectedResult); // FAIL BECAUSE I GET A LOGIN_SUCCESS INSTEAD OF A FAIL ONE
});
});
});
la réponse de Mark est correcte. Le middleware exécute ces instructions. Mais cela vous facilite la vie: dans le test, vous pouvez fournir tout ce que vous voulez comme argument à next()
, et le générateur La fonction le recevra à la suite de yield
. C'est exactement ce que fait le middleware de la saga (sauf qu'il déclenche en fait une demande au lieu de vous donner une fausse réponse).
Pour que yield
obtienne une valeur arbitraire, transmettez-la à next()
. Pour qu'il "reçoive" une erreur, transmettez-le à throw()
. Dans votre exemple:
it('should return a LOGIN_FAIL action', () => {
const action = {
payload: {
name: 'toto',
password: '123456'
}
};
const generator = login(action);
// Check that Saga asks to call the API
expect(
generator.next().value
).to.be.eql(
call(api.login, action)
);
// Note that *no actual request was made*!
// We are just checking that the sequence of effects matches our expectations.
// Check that Saga reacts correctly to the failure
expect(
generator.throw({
error: 'user not found'
}).value
).to.be.eql(
put({
type: 'LOGIN_FAIL',
payload: { error: 'user not found' }
})
);
});
Correct - si je comprends bien, tout l'intérêt de Redux-Saga est que votre fonction saga utilise les API saga pour renvoyer des objets décrivant l'action, puis le middleware examine plus tard ces objets pour exécuter réellement le comportement. Ainsi, une instruction yield call(myApiFunction, "/someEndpoint", arg1, arg2)
dans une saga peut renvoyer un objet qui ressemble théoriquement à {effectType : CALL, function: myApiFunction, params: [arg1, arg2]}
.
Vous pouvez soit inspecter la source redux-saga pour voir exactement à quoi ressemblent réellement ces objets déclaratifs et créer un objet correspondant à comparer dans votre test, soit utiliser les fonctions API elles-mêmes pour créer les objets (ce qui est, je pense, ce que redux-saga fait dans leur code de test).
Vous pouvez également utiliser une bibliothèque d'aide pour tester vos Sagas, comme redux-saga-testing .
Avertissement: j'ai écrit cette bibliothèque pour résoudre exactement le même problème
Cette bibliothèque fera ressembler votre test à n'importe quel autre test (synchrone), ce qui est beaucoup plus facile à raisonner qu'à appeler generator.next()
manuellement.
En prenant votre exemple, vous pouvez écrire des tests comme suit:
(il utilise la syntaxe Jest, mais c'est essentiellement la même chose avec Mocha, il est complètement indépendant de la bibliothèque de test)
import sagaHelper from 'redux-saga-testing';
import { call, put } from 'redux-saga/effects';
import actions from './my-actions';
import api from './your-api';
// Your example
export function* login(action) {
try {
const user = yield call(api.login, action);
return yield put(actions.loginSuccess(user));
} catch(e) {
yield put(actions.loginFail(e.message)); // Just changed that from "e" to "e.message"
}
}
describe('When testing a Saga that throws an error', () => {
const it = sagaHelper(login({ type: 'LOGIN', payload: 'Ludo'}));
it('should have called the API first, which will throw an exception', result => {
expect(result).toEqual(call(api, { type: 'LOGIN', payload: 'Ludo'}));
return new Error('Something went wrong');
});
it('and then trigger an error action with the error message', result => {
expect(result).toEqual(put(actions.loginFail('Something went wrong')));
});
});
describe('When testing a Saga and it works fine', () => {
const it = sagaHelper(login({ type: 'LOGIN', payload: 'Ludo'}));
it('should have called the API first, which will return some data', result => {
expect(result).toEqual(call(api, { type: 'LOGIN', payload: 'Ludo'}));
return { username: 'Ludo', email: '[email protected]' };
});
it('and then call the success action with the data returned by the API', result => {
expect(result).toEqual(put(actions.loginSuccess({ username: 'Ludo', email: '[email protected]' })));
});
});
Plus d'exemples (en utilisant Jest, Mocha et AVA) sur GitHub .