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.