Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test API request failures with Redux Saga?

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     });   }); }); 
like image 987
Ludo Avatar asked Feb 26 '16 14:02

Ludo


People also ask

How do you catch errors in Saga?

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.

What is done during saga testing?

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.

What is related to unit testing in redux saga?

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.

What is function * In redux saga?

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();}


2 Answers

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' }     })   ); }); 
like image 67
Dan Abramov Avatar answered Sep 28 '22 22:09

Dan Abramov


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).

like image 33
markerikson Avatar answered Sep 28 '22 23:09

markerikson