Mock mapStateToProps selectors and keep React unit tests simple

Matt Miller
7 min readJan 12, 2020

--

You’re building a React/Redux app. You’re doing your best to apply the best principles — separate containers, simple props passed down to your components. Then you run into a roadblock. Sooner or later, you’re going to be testing a component which renders a deeply nested tree of components. You find yourself mocking out a complex Redux store for every test, because some descendant components are connected to the store and require all kinds of different information.

The reality is, many of these components don’t really need all that data from the store, but just a subset of it. If you’re careful to design your components well, then they can delegate the work of extracting the bits of data that they need from the Redux store state in their mapStateToProps and mapDispatchToProps functions, keeping the component clean.

Here’s an approach where the store state is simply passed along to the component, which does the work of translating the raw values from the store into the data it needs. This makes the component needlessly complex. This work doesn’t necessarily need to be done inside of a React component — and it means that writing tests for this component means mocking out the Redux store and testing each path by actually rendering the component and inspecting its output.

// Doing the work in the componentexport const UsersList = ({ usersMap, currentUser, dispatch }) => {
const users = Object.values(usersMap)
.reduce((list, [id, user]) => ({
id,
canEdit: currentUser.permissions.editableGroups
.includes(user.group),
...user
}))
const handleCreateUser = data => dispatch({
type: 'CREATE_USER',
data
})
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
{user.canEdit ? <UserEditActions user={user} /> : null}
</li>
)}
<li><NewUserForm onCreateUser={handleCreateUser} /></li>
</ul>
)
}
export const UsersListContainer = connect(
state => ({
usersMap: state.users.usersMap,
currentUser: state.session.currentUser
})
)(UsersList)

We can actually make this component a lot easier to unit-test by moving this data logic into the mapStateToProps function. This has performance implications, but we can address that later. For now, let’s just worry about how we could refactor this component. We’ll let the component just deal with the UI-based logic, since that is the goal of a “presentational” component, and we’ll test our data translation — mapStateToProps — separately. It’s just a pure function, so it is very straightforward (and fast) to test.

export const UsersList = ({ users, onCreateUser }) => (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
{user.canEdit ? <UserEditActions user={user} /> : null}
</li>
)}
<li><NewUserForm onCreateUser={handleCreateUser} /></li>
</ul>
)
// Doing the work in mapStateToPropsexport const mapStateToProps = state => ({
users: Object.values(state.users.usersMap)
.reduce((list, [id, user]) => ({
id,
canEdit: state.session.currentUser.permissions.editableGroups
.includes(user.group),
...user
}))
})
export const mapDispatchToProps = dispatch => ({
onCreateUser: data => ({ type: 'CREATE_USER', data })
})
export const UsersListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(UsersList)

As you can see, our component became much simpler. If we export UsersList, we can test this component and only pass the exact data structure it requires. It doesn’t depend on Redux. Then, if we also export mapStateToProps and mapDispatchToProps, we can test the Redux side of things as well, knowing that they will translate our store data into props for UsersList correctly. We haven’t fully avoided having to mock a store for that purpose, but we’ve isolated that logic into testing very simple, pure functions that accept plain objects and return plain objects.

If we move mapStateToProps and mapDispatchToProps into a separate module, we can gain more advantages. First, we could make these functions reusable in other connected components. This would be accomplished by the functional programming concept of composition. Essentially, mapStateToProps might call several other defined functions that derive data from a store (selectors) and combine their results.

export const selectUsersList = state =>
Object.values(state.users.usersMap)
.reduce((list, [id, user]) => ({
id,
...user
}))
export const applyEditableToUsersList = (users, state) =>
users.map(user => ({
...user,
canEdit: state.session.currentUser.permissions.editableGroups
.includes(user.group)
})
export const selectColorScheme = state =>
state.session.currentUser.prefs.colorScheme
const mapStateToProps = state => ({
users: applyEditableToUsersList(
selectUsersList(state),
state
),
colorScheme: selectColorScheme(state)
})

There are more elegant ways to do this, but the gist is that reusing functions that translate store data into a format commonly used by one or more components reduces the areas of the application that would need to be rewritten should the store shape change. It also reduces our need to repeatedly mock a store data structure for many components which have similar mapStateToProps functions, since we can write them once, test them once, and easily compose them without creating any new logic that requires testing.

There is a much bigger second advantage, in my opinion. Specifically by moving your state selector functions into another module, we can mock these functions in our tests.

In reality, when testing a large application, you will inevitably render a component with deeply nested components, many of which will be connected to your store. While it is easy to export an un-connected component to test, this does not apply so well when its descendants are connected. We can use Enzyme to some degree to “shallow”-render our components in testing to avoid this problem, but sometimes this is not sufficient and does not let us target the right child components. In addition, with the growing popularity of React Testing Library, shallow rendering isn’t possible.

However, with a simple Jest mock, we could avoid creating an entire mock store and instead stub the results of our selectors. In other words, we can test our mapStateToProps functions independently, then only stub the outputted props of those functions when testing our React components.

Let’s take a look at an example.

const UsersPage = () => (
<div className="page">
<UsersListContainer />
</div>
)

This component is not connected, but it contains a child component that is connected. Normally, it would be impossible to test the UsersPage component without mocking the store. Depending on the children of this component and the descendants of all their components, the various connected pieces may require a lot of input from many different areas of the store. As the application grows, testing this component might require a very large mocked Redux store. However, these components don’t necessarily need all of the data passed into their selectors — the props that result from our selectors will be only a small, simplified subset of the data.

If we are able to test using only those props rather than the store, we have less to reason about. When reading a unit test for a component, we don’t have to look at the mocked store and then mentally trace the translation of that data structure into all the various props flowing into all the nested components. Instead, we can control that data by replacing those selectors with functions that just return straightforward values.

// Testing the component by mocking the storeconst store = createStore(() => {}, {
users: {
'123': { name: 'User 1', group: 'A' },
'456': { name: 'User 2', group: 'B' }
},
session: {
currentUser: {
permissions: {
editableGroups: ['B']
}
}
}
})
const result = render(
<Provider store={store}>
<UsersPage />
</Provider>
)
// Testing the component by mocking mapStateToPropsconst onCreateUser = jest.fn()jest.mock('./users-list/selectors', () => ({
__esModule: true,
mapStateToProps: () => ({
users: [
{ id: '123', name: 'User 1', group: 'A', canEdit: false },
{ id: '456', name: 'User 2', group: 'B', canEdit: true }
]
}),
mapDispatchToProps: () => ({ onCreateUser })
})
const result = render(
<UsersPage />
)

This method also encourages us to only pass to our components the specific data we need, rather than selecting too much from the store and reducing the relevant data within the component. Now, looking at the component, we know exactly what props it needs to run. If a child component requires more data to be passed along, we might choose to connect it as well to derive its own data, instead of selecting more data than needed by a specific component for the sake of its children, which creates a tightly-coupled scenario. Where we might avoid connect simply for the sake of testing pure components, we can now test normally. The biggest caveat is that, due to how mocking typically works in JavaScript, our mapStateToProps functions can’t exist in the same module as the components and containers.

We still need to test the logic of our selectors, but we can do so without simultaneously testing all of the UI-based logic that is specific to each component that is connected. We assert that the props we receive match what we expected, rather than inferring that by looking at the rendered output. For example, we could test that a selector returned loaded: false instead of testing a specific component’s behavior when the prop loaded is false (component X renders a spinner, component Y returns null, etc.).

assert.strictEqual(
mapStateToProps({
users: {
'123': { name: 'User 1', group: 'A' },
'456': { name: 'User 2', group: 'B' }
},
session: {
currentUser: {
permissions: {
editableGroups: ['B']
}
}
}
}),
{
users: [
{ id: '123', name: 'User 1', group: 'A', canEdit: false },
{ id: '456', name: 'User 2', group: 'B', canEdit: true }
]
}
)

We haven’t fully avoided mocking the store, but we have less to reason about in terms of input and output, and if we compose our selectors, we can avoid testing similar situations over and over again simply because different components in our app selected the same data from the store.

The final step is to address the performance implication I mentioned before. The connect function is able to cache by finding references in the values of the returned props object and re-rendering when those references no longer match data in the store. However, when we translate data, it becomes never equal to those original references. This can mean that any update to the Redux store could trigger re-renders, if mapStateToProps is returning new data each time.

We can solve this problem with the Reselect library, which I have gone over previously in an article about testing container components.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Matt Miller
Matt Miller

No responses yet

Write a response