Bloomca
Bloomca

Reputation: 1794

Testing Complex Asynchronous Redux Actions

So, let's say I have the next action:

export function login({ email, password, redirectTo, doNotRedirect }) {
  return ({ dispatch }) => {
    const getPromise = async () => {
      const basicToken = Base64.encode(`${email}:${password}`);
      const authHeaders = { Authorization: `Basic ${basicToken}` };
      const { payload, error } = await dispatch(sendAuthentication(authHeaders));

      if (error) throw payload;

      const { username, token, fromTemporaryPassword } = payload;
      const encodedToken = Base64.encode(`${username}:${token}`);

      dispatch(persistence.set('authorizationToken', encodedToken));
      dispatch(postGlobalId({ username }));
      dispatch(setIsLoggedIn(true));
      dispatch(setIsFromTemporaryPassword(fromTemporaryPassword));

      await dispatch(clientActions.fetchClient);

      if (doNotRedirect) return;

      if (fromTemporaryPassword)
        dispatch(updatePath('/profile/change-password'));
      else
        dispatch(updatePath(redirectTo || '/dashboard'));
    };

    return {
      type: AUTHENTICATION_LOGIN,
      payload: getPromise()
    };
  };
}

And I want to add tests for it, to add reliability to the code.

So, here are few things:

  1. We send authentication headers and get data as a response
  2. We throw an error if some error is present in the response
  3. We set up all needed tokens, dispatch all needed actions to show that we are logged in now
  4. Fetching client data
  5. Based on params and received data, we redirect to needed route / don't redirect

The question is that it is really too hard to test and we need to stub literally everything, which is bad due to brittle tests, fragility and too much of implementation knowing (not to mention that it is pretty challenging to stub dispatch to work properly).

Therefore, should I test all of these 5 points, or to focus only on the most important stuff, like sending authorization request, throw error and check redirects? I mean, the problem with all flags that they can be changed, so it is not that reliable.

Another solution is just to separate these activities into something like following:

And to pass all needed functions to invoke through dependency injection (here just with params, basically)? With this approach I can spy only invoking of this functions, without going into much details.

I am quite comfortable with unit testing of pure functions and handling different edge-cases for them (without testing too much implementation, just the result), but testing complex functions with side-effects is really hard for me.

Upvotes: 3

Views: 179

Answers (1)

TomW
TomW

Reputation: 4002

If you have very complex actions like that, I think an alternative (better?) approach is to have simple synchronous actions instead (you can even just dispatch payloads directly, and drop action creators if you like, reducing boiler-plate), and handle the asynchronous side using redux-saga: https://github.com/yelouafi/redux-saga

Redux Saga makes it very simple to factor out your business logic code into multiple simple generator functions that can be tested in isolation. They can also be tested without the underlying API methods even being called, due to the 'call' function in that library: http://yelouafi.github.io/redux-saga/docs/api/index.html#callfn-args. Due to the use of generators, your test can 'feed' values to the saga using the standard iterator.next method. Finally, they make it much easier for reducers to have their say, since you can check something from store state (e.g. using a selector) to see what to do next in your saga.

If Redux + Redux Saga had existed before I started on my app (about 100,000 JS(X) LOC so far), I would definitely have used them.

Upvotes: 3

Related Questions