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 }); }); });
We can catch errors inside the Saga using the familiar try/catch syntax. In this case, we're passing the throw method a fake error. This will cause the Generator to break the current flow and execute the catch block. Of course, you're not forced to handle your API errors inside try / catch blocks.
To test your saga, the sagaTester instance start() method with your saga and its argument(s). This runs your saga to its end. Then you may assert that effects occured, actions were dispatched and the state was updated as expected.
Redux saga is a very useful middleware which handles our application side effects like api calls in your react redux data flow. In simpler terms they sit between your actions and reducers and handle all asynchronous logic for you as plain redux action isn't capable of that.
In Redux saga, yield is a built in function which allows to use generator functions sequentially. When used in Javascript, the generator functions will allow to yield all values from the nested functions. function* one(){}function* abc(){const result = yield* one();}
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 of yield
. 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 to next()
. To make it “receive” an error, pass it to throw()
. In your example:
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 - 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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With