3

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:

  • auth
  • setLoginInfo
  • handleRedirects

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.

7
  • My gut feeling is that you should take a large amount of the logic out of your thunk action creator and place it into separate utility functions that are called by the action creator. Each of the 5 steps you have outlined should have their own tests. Commented Dec 19, 2016 at 12:40
  • But it is not a utility function, it is exactly business logic. Yeah, I can make them pure – but I basically need to check that they are invoked, otherwise it is too many of implementation details. Commented Dec 19, 2016 at 14:56
  • What do you mean? All I am saying is that this action creator has too much logic in it. You should make extra functions that will server as utility functions to the action creator in order to improve modularity. Commented Dec 19, 2016 at 16:26
  • They can't be utility functions, because all of them have side effects. The problem is how to separate them and how to test the whole flow. Commented Dec 20, 2016 at 12:00
  • Can you link to me a resource that says utility functions can't have side effects? Commented Dec 20, 2016 at 12:19

1 Answer 1

3
+25

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.