At Ximedes, we’ve built many React apps and use Redux for almost all our front-end related projects. For most React projects the Mocha/Chai/Sinon test stack is used, because competitor Jest was just not as good. However, it really started to catch up lately in terms of quality. This blog shows how we test Redux-thunk action creators, using only Jest.
Let’s begin.
Say we have a user login screen that comes along with the following Redux action creators:
import axios from 'axios';
import { LoginType } from './ActionTypes';
export function loginButtonPressed(username, password) {
return async (dispatch, getState) => {
const url = 'https://endpoint.local';
dispatch(authenticationPending());
try {
await axios.post(url, {
username,
password
});
dispatch(authenticationSuccess());
} catch (error) {
dispatch(authenticationFailed());
}
};
}
export function authenticationPending() {
return {
type: LoginType.AUTHENTICATION_PENDING
};
}
export function authenticationSuccess() {
return {
type: LoginType.AUTHENTICATION_SUCCESS
};
}
export function authenticationFailed() {
return {
type: LoginType.AUTHENTICATION_FAILED
};
}
This is a typical set of action creators:
- One asynchrous action creator (thunk) which deals with async HTTP requests.
- Three synchronous action creators which deal with:
- waiting on the HTTP response (authenticationPending),
- when the HTTP response is OK (authenticationSuccess),
- failure (authenticationFailed).
When the user clicks the login button, loginButtonPressed()
is called and LoginType.AUTHENTICATION_PENDING
is dispatched. When the authentication is successful, LoginType.AUTHENTICATION_SUCCESS
is dispatched or else LoginType.AUTHENTICATION_FAILED
when the authentication failed.
Async/Await
The async/await syntax is a just cleaner way of saying .then().catch()
. When await loginButtonPressed('test_user', 'test_password')(dispatch, getState);
is called, executing the next line of code is halted until a result has come back from axios.post()
. If the Promise is resolved, execution continues. If it is rejected, the execution stops and an error is thrown and caught.
The code to test the success/happy flow of loginButtonPressed()
, is below. Let’s go through it step-by-step:
import axios from 'axios';
import { loginButtonPressed } from '../../main/actions/userActions';
import { LoginType } from '../../main/actions/ActionTypes';
describe('userActions', () => {
it('Login successful, creates AUTHENTICATION_PENDING and AUTHENTICATION_SUCCESS', async () => {
const expected = [
{ type: LoginType.AUTHENTICATION_PENDING },
{ type: LoginType.AUTHENTICATION_SUCCESS }
];
// mock the axios.post method, so it will just resolve the Promise.
axios.post = jest.fn(url => {
return Promise.resolve();
});
// mock the dispatch and getState functions from Redux thunk.
const dispatch = jest.fn(),
getState = jest.fn(() => {
url: 'https://endpoint.local';
});
// execute
await loginButtonPressed('test_user', 'test_password')(dispatch, getState);
// verify
expect(dispatch.mock.calls[0][0]).toEqual(expected[0]);
expect(dispatch.mock.calls[1][0]).toEqual(expected[1]);
});
});
First at const expected =
, we expect two actions to be dispatched:
LoginType.AUTHENTICATION_PENDING
LoginType.AUTHENTICATION_SUCCESS
Now at axios.post = jest.fn((url)
, we make use of Jest’s included function mocking. We mock out the HTTP POST request with
jest.fn(() => Promise.resolve();)
This will simulate a successful HTTP response. Furthermore we mock both thunk functions, dispatch()
and getState()
. The function getState()
should return a Redux state object with a URL needed for the axios POST request.
Next, we call loginButtonPressed()
. As a Redux thunk is nothing more than a function that returns a function, we call the result of loginButtonPressed()
immediately again with our mocked functions as arguments: dispatch
and getState
.
Lastly, assertion is done. Each Jest mocked function gets a mock property, in where all sorts of information about the function is stored. E.g. with which argument it is called. We assert dispatch()
to have been called with:
{ type: LoginType.AUTHENTICATION_PENDING }
{ type: LoginType.AUTHENTICATION_SUCCESS }
That’s it! We have used only Jest to test action creators, instead of the Mocha, Chai and Sinon stack. This stack used to be one of the best to use, but since Jest was rewritten begin 2016, it has gotten much better.