Testing Request Reducer - Part 2


In this video we cover the process for stopping requests. As we saw in the previous video, whenever we start a request we add an item to our array of inProgress requests. Therefore whenever we finish a request - successfully or not - we want to ensure that the corresponding request is removed from the inProgress requests array.

Again, as in the previous video we must find a way to update our current state without mutating the existing state in any way. We'll cover how to do this as we go through, but first, let's look at the first test we need to take from red to green:

// /__tests__/reducers/requestReducer.react-test.js

import request from '../../src/reducers/requestReducer';
import {REQUEST__STARTED, REQUEST__FINISHED} from '../../src/constants/actionTypes';

describe('Request Reducer', () => {

  // * snip *

  it('can handle REQUEST__FINISHED', () => {

    let state = {
      sendingRequest: true,
      inProgress: ['some.saga']
    };

    let action = {
      type: REQUEST__FINISHED,
      payload: {
        requestFrom: 'some.saga'
      }
    };

    expect(request(state, action)).toEqual({
      sendingRequest: false,
      inProgress: []
    });

  });
});

As mentioned, the idea here is to remove a request - by name - from the array on inProgress requests. Also, we would like to update our convenience property of sendingRequest to false whenever there are no more elements in the inProgress array. This saves us from having to do any logic in our code to determine this value.

When we only have one item in the array, getting back to the 'empty' / initial state is really easy. Therefore we could cheat a little and simply hardcode the values in:

// /src/reducers/requestReducer.js

import {REQUEST__STARTED, REQUEST__FINISHED} from '../constants/actionTypes';

export default function request(state = {
  sendingRequest: false,
  inProgress: []
}, action) {

  switch (action.type) {

    case REQUEST__STARTED: {
      return Object.assign({}, state, {
        sendingRequest: true,
        inProgress: state.inProgress.concat([action.payload.requestFrom])
      });
    }

    case REQUEST__FINISHED: {
      return Object.assign({}, state, {
        sendingRequest: false,
        inProgress: []
      });
    }

    default: {
      return state;
    }
  }
}

At this stage we could have a passing test.

However, of course this implementation falls down when we have two requests in progress:

// /__tests__/reducers/requestReducer.react-test.js

import request from '../../src/reducers/requestReducer';
import {REQUEST__STARTED, REQUEST__FINISHED} from '../../src/constants/actionTypes';

describe('Request Reducer', () => {

  // * snip *

  it('can handle multiple instances of REQUEST__FINISHED', () => {

    let state = {
      sendingRequest: true,
      inProgress: ['some.saga', 'another.saga']
    };

    let action = {
      type: REQUEST__FINISHED,
      payload: {
        requestFrom: 'another.saga'
      }
    };

    let newState = request(state, action);

    expect(newState).toEqual({
      sendingRequest: true,
      inProgress: ['some.saga']
    });

    action = {
      type: REQUEST__FINISHED,
      payload: {
        requestFrom: 'some.saga'
      }
    };

    newState = request(newState, action);

    expect(newState).toEqual({
      sendingRequest: false,
      inProgress: []
    });

  });

});

If we try this, we would find the expect step fails:

    expect(newState).toEqual({
      sendingRequest: true,
      inProgress: ['some.saga']
    });

As we would have fallen back to initial values regardless of how many requests were inProgress.

Ok, so fixing this is a two step process.

The first thing we need to do is remove the given request from the inProgress array.

Then, we can use length of the inProgress array to determine if sendingRequest should be true or false. That is to say, if the .length of inProgress is greater than zero, by all accounts we must still have at least one request in progress.

A key point is that we do not want to mutate any existing state. A way to achieve this is to use the filter function, which allows us to loop through each element / item in our array, and run a predicate against the item. A predicate is a function that returns true or false. If the check is true, the item should be filtered. If the check is false, we keep the item. Any items that are kept are stored into a new array.

A new array? Awesome, that means we won't mutate state.

The filtering function is easy enough:

// /src/reducers/requestReducer.js

import {REQUEST__STARTED, REQUEST__FINISHED} from '../constants/actionTypes';

export default function request(state = {
  sendingRequest: false,
  inProgress: []
}, action) {

  switch (action.type) {

    // * snip *

    case REQUEST__FINISHED: {

      let stillInProgress = state.inProgress.filter((item) => item !== action.payload.requestFrom);

      return Object.assign({}, state, {
        sendingRequest: stillInProgress.length > 0,
        inProgress: stillInProgress
      });
    }

    // * snip *
  }
}

Simply we loop through each item in the inProgress array, and if that item is not a match of the requestFrom string, we keep it.

Then, we can use this new array's .length to return a true or false value for sendingRequest.

And with that, we should have two further passing tests, covering all case statements in our request reducer.

Code For This Course

Get the code for this course.

Episodes