Testing async redux with third party API calls

587 views Asked by At

I'm new to redux and programming in general and am having trouble wrapping my head around certain unit testing concepts.

I have some async actions in redux, which involve calls to a third party API (from the 'amazon-cognito-identity-js' node module).

I have wrapped the external API call in a promise function, and I call this function from the 'actual' action creator. So for testing I just want to stub the result of externalAWS() function so that I can check that the correct actions are being dispatched.

I'm using redux-thunk for my middleware.

import { AuthenticationDetails,
         CognitoUser
} from 'amazon-cognito-identity-js';

export function externalAWS(credentials) {

  //This is required for the package
  let authenticationDetails = new AuthenticationDetails(credentials);

  let cognitoUser = new CognitoUser({
  //Construct the object accordingly
  })

  return new Promise ((resolve, reject) => {

    cognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: result => {
        resolve(result);
      },
      onFailure: error => {
        reject(error)
      }
    })
  }
}

export function loginUser(credentials) {

  //return function since it's async
  return dispatch => {

    //Kick off async process
    dispatch(requestLogin());

    externalAWS(credentials)
      .then((result) => {
        dispatch(receiveLogin(result.getAccessToken().getJwtToken(), credentials.username))
      })
      .catch((error) => {
        dispatch(failedLogin(error.message, etc))
      })
  }
}

I don't have any test code yet because I am really not sure how to approach this. All the examples deal with mocking a HTTP request, which I know is what this boils down to, so am I supposed to inspect the HTTP requests in my browser and mock them out directly?

It's further complicated by the fact that the second argument of authenticateUser is not even a plain callback, but an object with callbacks as it's values.

Can anyone offer some advice on whether my intention in unit testing the async function is correct, and how I should approach it? Thank you.

Edit: I'm testing in Jest.

Edit2: Request Headers First POST request, Second POST request

Edit3: Split the function, trying my best to isolate the external API and create something that is 'easily mock/stub-able'. But still running into issues of how to properly stub this function.

2

There are 2 answers

0
Thomas Chia On BEST ANSWER

So I figured it out in the end. First, I had to require() the module into my test file (as opposed to ES6 import). Then I removed the promise for now since it was adding a layer of complexity and combined everything into one function, let's call it loginUser(). It is a redux async action, that dispatches one action upon being called, and then a success or failure action depending on the result of the API call. See above for what the API call looks like.

Then I wrote the test as follows:

const CognitoSDK = require('/amazon-cognito-identity-js')
const CognitoUser = CognitoSDK.CognitoUser

//Set up the rest of the test

describe('async actions', (() => {
  it('should dispatch ACTION_1 and ACTION_2 on success', (() => {
    let CognitoUser.authenticateUser = jest.fn((arg, callback) => {
      callback.onSuccess(mockResult)
    })
    store.dispatch(loginUser(mockData))
    expect(store.getActions()).toEqual([{ACTION_1}, {ACTION_2}])
  }))
}))

So basically once requiring the module in, I mocked it in Jest and did a mock implementation too, so that I could access the onSuccess function of the callback object.

14
therewillbecode On

Redux thunk gives you the ability to dispatch future actions within the context of a main action that kicks off the process. This main action is your thunk action creator.

Therefore tests should focus on what actions are dispatched within your thunk action creator according to the outcome of the api request.

Tests should also look at what arguments are passed to your action creators so that your reducers can be informed about the outcome of the request and update the store accordingly.

To get started with testing your thunk action creator you want to test that the three actions are dispatched appropriately depending on whether login is successful or not.

  1. requestLogin
  2. receiveLogin
  3. failedLogin

Here are some tests I wrote for you to get started using Nock to intercept http requests.

Tests

import nock from 'nock';

const API_URL = 'https://cognito-idp.us-west-2.amazonaws.com/'

const fakeCredentials = {
    username: 'fakeUser'
    token: '1234'
}

it('dispatches REQUEST_LOGIN and RECEIVE_LOGIN with credentials if the fetch response was successful', () => {

  nock(API_URL)
    .post( ) // insert post request here  e.g - /loginuser
    .reply(200, Promise.resolve({"token":"1234", "userName":"fakeUser"}) })

  return store.dispatch(loginUser(fakeCredentials))
    .then(() => {
      const expectedActions = store.getActions();
      expect(expectedActions.length).toBe(2);
      expect(expectedActions[0]).toEqual({type: 'REQUEST_LOGIN'});
      expect(expectedActions[1]).toEqual({type: 'RECEIVE_LOGIN', token: '1234', userName: 'fakeUser'});
    })
});

it('dispatches REQUEST_LOGIN and FAILED_LOGIN with err and username if the fetch response was unsuccessful', () => {

  nock(API_URL)
      .post( ) // insert post request here  e.g - /loginuser
      .reply(404, Promise.resolve({"error":"404", "userName":"fakeUser"}))

  return store.dispatch(loginUser(fakeCredentials))
    .then(() => {
      const expectedActions = store.getActions();
      expect(expectedActions.length).toBe(2);
      expect(expectedActions[0]).toEqual({type: 'REQUEST_LOGIN'});
      expect(expectedActions[1]).toEqual({type: 'FAILED_LOGIN', err: '404', userName: 'fakeUser'});
    })
});