I am trying to test every scenarios my saga could follow, but i am not able to make happens the behaviors i want. This is pretty simple, i have a HTTP request (login), and i want to test the success and the failure cases by mocking my API method.
But, it looks like the call effect
doesn't fire my api function, i don't really get yet how it works, but i guess that the middleware is in charge of invoking the function, and since i don't go though the store on my test, i can't get the result.
So my question is, how can you test your saga when you need to dispatch different actions (typically success or failure) next to your async call ?
I looked for an example, i found sagas with success and fail but the fail case is never tested, for example in the shopping cart example here
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
});
});
});
You also might want to use a helper library to test your Sagas, such as redux-saga-testing.
Disclaimer: I wrote this library to solve that exact same problem
This library will make your test look like any other (synchronous) test, which is a lot easier to reason about than calling
generator.next()
manually.Taking your example, you could write tests as follow:
(it's using Jest syntax, but it's essentially the same with Mocha, it's completely test library-agnostic)
More examples (using Jest, Mocha and AVA) on GitHub.
Correct - as I understand it, the whole point of Redux-Saga is that your saga function uses the saga APIs to return objects describing the action, and then the middleware later looks at those objects to actually execute the behavior. So, a
yield call(myApiFunction, "/someEndpoint", arg1, arg2)
statement in a saga might return an object that notionally looks like{effectType : CALL, function: myApiFunction, params: [arg1, arg2]}
.You can either inspect the redux-saga source to see exactly what those declarative objects actually look like and create a matching object to compare against in your test, or use the API functions themselves to create the objects (which is I think what redux-saga does in their test code).
Mark’s answer is correct. Middleware executes those instructions. But this makes your life easier: in the test, you can provide whatever you want as the argument to
next()
, and the generator function will receive it as a result ofyield
. This is exactly what saga middleware does (except that it actually fires up a request instead of giving you a fake response).To make
yield
get an arbitrary value, pass it tonext()
. To make it “receive” an error, pass it tothrow()
. In your example: