- (Im)pure functions and why they matter
- React/Redux architecture from a (im)pure function perspective
- Testing synchronous actions
- Testing asynchronous actions
- Testing reducers
- Testing components
- Testing containers
- How to build and run the demo application
- Links to useful resources
- Don’t cause side effect
- Don’t access global state
- Given the same input, they're guaranteed to always produce the same output
- Easy to test
- Predictable
- Don’t require mocks and/or interceptors
- In React/Redux applications, sync actions, reducers and presentational components should all be pure functions.
/* This is an example of a pure function. Notice that it does not mutate state, does not depend on global state and given the same input (user in this case), it will always produce the same output. */
function createUserSuccess(user) {
return {
type: CREATE_USER_SUCCESS,
payload: { user }
};
}
- Cause side effects
- Mutate global state
- Hard to test
- Not predictable
- Require mocks and/or interceptors
- In React/Redux applications, async actions and containers are impure functions
/* Notice that it interacts with the outside world, namely the backend and the store */
function createUser(user) {
return function(dispatch) {
dispatch(createUserRequest());
return axios.post('/api/users', user)
.then(res => dispatch(createUserSuccess(res.data)))
.catch(err => dispatch(createUserFailure(err.response.data)));
}
}
- A sync action would say: “Here’s a description of how the state should be changed, along with some data”
- Sync action creators are pure functions and they are very easy to test
/* this is a sync action creator */
function createUserFailure(err) {
return {
type: CREATE_USER_FAILURE,
payload: { err }
};
}
/* this is how you test it */
it('should create action to inform that a user failed to be created', () => {
const err = { message: 'test message' };
const expectedAction = { type: CREATE_USER_FAILURE, payload: { err } };
const actualAction = createUserFailure(err);
expect(actualAction).toEqual(expectedAction);
});
- An async function would say “Let me first talk to the backend first, ok? Then I can follow up with the store with description(s) of how the state should be changed, along with some data”
- Sync actions are inpure, because most of the time they interact with the backend and dispatch actions to mutate the Redux store.
/* This is an async action creator. Notice that it makes an http call to the backend with axios and dispatches various actions to mutate the store. */
export function createUser(user) {
return function(dispatch) {
dispatch(createUserRequest());
return axios.post('/api/users', user)
.then(res => dispatch(createUserSuccess(res.data)))
.catch(err => dispatch(createUserFailure(err.response.data)));
}
}
/* This is how you test it. First you create an http interceptor with nock, create a mock store and then assert that the expected actions were dispatched to the store. */
it('should create action to create user', () => {
const user = { name: 'test', email: '[email protected]' };
nock('http://localhost').post('/api/users', user).reply(200, user);
const store = mockStore({ user: { list: [] } });
return store.dispatch(createUser(user))
.then(() => {
expect(store.getActions()).toEqual([
{ type: CREATE_USER_REQUEST },
{ type: CREATE_USER_SUCCESS, payload: { user } }
]);
});
});
- A reducer would say: “Just give me the current state and a description of what should be change along with data and I’ll change the part of the state I’m responsible for”
- Reducers are pure functions and should be very easy test.
/* This is the switch case in which a create user get's added to the store */
case CREATE_USER_SUCCESS:
return {
...state,
list: [payload.user, ...state.list],
isCreatingUser: false,
createUserFailureMessage: undefined
};
- A component would say: “Give me my props, and I’ll give you a representation of the DOM”
- Components should be easy to test, since they are pure functions that given the same props, will always return the same representation of the DOM.
/* This is the user list components. */
function UserListItem({user, deleteUser, deletingUserId}) {
return (
<tr key={user.id}>
<td data-r-test="user-list-item-name">{user.name}</td>
<td data-r-test="user-list-item-email">{user.email}</td>
<td style={{width: 65, textAlign: 'right'}}>
<button
data-r-test="user-list-item-delete-button"
className="btn btn-xs btn-danger"
type="button"
disabled={deletingUserId === user.id}
onClick={() => deleteUser(user.id)}
>{deletingUserId === user.id ? 'deleting...' : 'delete'}</button>
</td>
</tr>
);
}
/* This is how you test a presentational component */
it('should display user\'s email', () => {
const props = { ...baseProps };
const wrapper = shallow(<UserListItem {...props} />);
expect(
wrapper.find('[data-r-test="user-list-item-email"]').text()
).toEqual(props.user.email);
});
it('should delete user', () => {
const deleteUser = jest.fn();
const props = {
...baseProps,
deleteUser
};
const wrapper = shallow(<UserListItem {...props} />)
wrapper.find('[data-r-test="user-list-item-delete-button"]').simulate('click');
expect(deleteUser).toHaveBeenCalledWith(props.user.id);
});
- A comtainer would say: “Hey component, here’s some data from the store and functions/actions you can call”
- Containers are impure functions because they are dependant on the global context of react
function mapStateToProps(state) {
return {
userList: state.user.list,
isFetchingUser: state.user.isFetchingUser
};
}
function mapDispatchToProps(dispatch) {
dispatch(fetchUsers());
return {};
}
function UserListContainer({ userList, isFetchingUser }) {
return isFetchingUser
? (<div className="alert alert-info">Fetching users...</div>)
: <UserList userList={userList} />;
}
export default connect(mapStateToProps, mapDispatchToProps)(UserListContainer);
/* This is how you test that the container passes the expected props to user_list.js, and */
describe('Userlist container', () => {
it('should dispatch action to fetch users', () => {
const wrapper = mount(
<Provider store={store}>
<UserListContainer />
</Provider>
);
const expectedActions = [{"type": "FETCH_USERS_REQUEST"}];
const actualActions = store.getActions();
expect(expectedActions).toEqual(actualActions);
});
it('should connect UserList component to Redux store', () => {
const state = {
user: {
list: [
{ name: 'test1', email: '[email protected]', id: 1 },
{ name: 'test2', email: '[email protected]', id: 2 },
]
}
};
const store = mockStore(state);
const wrapper = mount(
<Provider store={store}>
<UserListContainer />
</Provider>
).find('UserList').first();
expect(wrapper.props().userList.length).toEqual(state.user.list.length);
});
});
git clone [email protected]:leocristofani/testing-react-redux-applications.git
cd into testing-react-redux-applications
npm install
npm run build
node server/index.js
PS. This application was build with Create React App. Refer to the docs for further actions.
- Writing tests section of the Redux docs
- Presentational and container components
- What is a pure function
- Redux DevTools (Chrome extension)
- Jest https://facebook.github.io/jest/
- Enzyme https://github.com/airbnb/enzyme
- Mock-redux-store http://arnaudbenard.com/redux-mock-store/
- Nock https://github.com/node-nock/nock