Luke Pierotti

@lukepierotti

Unit Testing Redux Connected Components

Mocking functions and Redux store

Testing React+Redux with Jest+Enzyme

When I did my previous post, I purposefully left out how to test connected components. With components that are connected to a Redux store, it takes a little bit more effort to set up test files and write tests. For this example I am using Jest and Enzyme to do my testing. I’ll talk a little about how to test these components using mock stores and mock functions.

How do we test them?

I’m going to continue using my Login component as an example. The code for the component looks like this,

// app/src/components/Login.js
import React from 'react'
import { connect } from 'react-redux'
import { loginUser } from '../actions/users'
class Login extends React.Component {
constructor() {
super()
this.state = {
username: '',
password: ''
}
}
handleInputChange = (event) => {
this.setState({
[event.target.name]: event.target.value
})
}
handleSubmit = (event) => {
event.preventDefault()
this.props.login(this.state)
}
render() {
return (
<form id='loginForm' className='login' onSubmit={this.handleSubmit}>
<label>Username</label>
<input id='email' onChange={this.handleInputChange} name='email' type='text' />
<label>Password</label>
<input id='password' onChange={this.handleInputChange} name='password' type='password' />
<button>Submit</button>
</form>
)
}
}
function mapDispatchToProps(dispatch) {
return {
login: (userparams, history) => {
dispatch(loginUser(userparams, history))
}
}
}
export default connect(mapDispatchToProps)(Login)

I connected this component to my Redux store and added an onSubmit event for the form which calls the handleSubmit function. I added the mapDispatchToProps function which returns my login function. This will call on my action creator and dispatch my login user action. When the form gets submitted, it should call my login function from the props which will be responsible for logging in the user.

When trying to write a test for a connected component you will probably encounter this error message,

This message is a result of Jest trying to find your Redux store, but being unable to find it. This makes sense because our test files are separate from the app so we haven’t created a store or set up Redux.

How do we fix this problem?

As I was researching I came across a couple different ways of fixing this problem. There are two main options, both with support from different groups. Mostly I saw a divide between testing the component as it is connected to Redux, or to test the component when not connected to Redux. I’ll talk a little about each approach and tell you what I prefer.

Testing component with mock store

This way of testing your component, in my opinion, is not suggested. When using this approach, we are over stepping the boundaries of unit testing and instead starting to test integration. I’ll show you how to set up a mock store, but I wont show any tests with this approach.

As you can see the error message above gives us two ways in which we can fix our problem. We can explicitly create a store and pass it to our connected Login component, or we can wrap the Login component in a Redux Provider that holds the store. I would suggest using the first approach, because then there is no need to mount our component and we can instead shallow render it. Mounting requires Enzyme to render the full DOM, while shallow rendering requires only the component to be rendered.

One way that we can integrate the store is by using redux-mock-store to create it. To add this to you project run

npm install redux-mock-store --save-dev

This will allow us to create a store to pass to our connected component. This is how we could create a mock store,

// app/src/components/__tests__/Login-test.js
import configureStore from 'redux-mock-store'
// create any initial state needed
const initialState = {};
// here it is possible to pass in any middleware if needed into //configureStore
const mockStore = configureStore();
let wrapper;
let store;
beforeEach(() => {
  //creates the store with any initial state or middleware needed  
store = mockStore(initialState)
wrapper = see below...
})

And the two ways of passing it to our component are like this,

wrapper = shallow(<Login store={store}/>)
OR
// not suggested
wrapper = mount(<Provider store={store}<Login /></Provider>)

If you look at the Redux Mock Store docs, the reason the library was created was to test action creators and middleware. Already this seems like a red flag when we are using the library in a way it wasn’t originally designed for.

With this mock store, we can test our actions that are sent to our store through action creators, and see if they match the expected actions. This is more of an integration test, and testing the whole flow of our component. It is more complex and unnecessary, than if you test all parts of the application separately. I feel there is a much easier way of testing our component to make sure it is operating properly with Redux.

Testing component without Redux store

If we test the component when it is not connected to the Redux store, we do not have to worry about the extra work of creating a mock store. Instead we just test the functionality of our component, and see if it behaves how we expect. To do this we can simply export our unconnected component, as well as our default export of the connected component. We do this by adding export before our component like this,

// app/src/components/Login.js
import React from 'react'
import { connect } from 'react-redux'
// add export here to export the unconnected component
export class Login extends React.Component {
    // ...code above
}
function mapDispatchToProps(dispatch) {
return {
login: (userparams, history) => {
dispatch(loginUser(userparams, history))
}
}
}
export default connect(mapDispatchToProps)(Login)

Now when we import the component into our test we have to write it like this,

// app/src/components/__tests__/Login-test.js
import { Login } from '../Login'

This imports the unconnected component rather than the default connected component.

Because we do not have a store, our mapDispatchToProps function for logging in is not supplied to our component. We can easily fix this by creating a mock function with Jest. This function will hold the place of our action creator, and we will be able to test whether it gets called. To do this we can add this code to our test file, and pass this function as our login prop,

// app/src/components/__tests__/Login-test.js
describe('Login Component', () => {
let wrapper;
// our mock login function to replace the one provided by mapDispatchToProps
const mockLoginfn = jest.fn();

beforeEach(() => {
// pass the mock function as the login prop
wrapper = shallow(<Login login={mockLoginfn}/>)
})
// ...tests here...
}

This replaces the login function passed to our component by mapDispatchToProps. It is not necessary to test that our mapDispatchToProps is properly passing our login function to the connected component, because Redux is already responsible for this. So now that our component has that function, we can test whether our function gets called when we submit our login form. The code for the test looks like this,

// app/src/components/__tests__/Login-test.js\
describe('When the form is submitted', () => {
it('should call the mock login function', () => {
wrapper.find('#loginForm').simulate(
'submit',
{preventDefault() {}}
)
expect(mockLoginfn.mock.calls.length).toBe(1)
})
})

When we simulate the submission of our form, we need to pass the preventDefault function in our event object or we will get an error. This is because the handleSubmit function will call event.preventDefault(), and if we don’t include it an error occurs. Once we simulate the submit event, we can then test our mock login function to see if it was called. Our mockLoginfn is essentially a spy like you would have if you were using the Sinon library. So by checking to make sure our mockLoginfn was called once, we are verifying that when we submit the form it will call the correct prop function and send the correct action. We can also add a test where we fill in the username and password fields, and then simulate the submission. When we do that we can check our mockLoginfn to make sure it was passed the correct arguments. That would look like this,

it('should be called with the email and password in the state as arguments', () => {
   // fill in email field with blah@gmail.com     
wrapper.find('#email').simulate(
'change',
{target:
{name: 'email', value: 'blah@gmail.com'}
}
)
   // fill in password field with cats  
wrapper.find('#password').simulate(
'change',
{target:
{name: 'password', value: 'cats'}
}
)
   // simulate form submission   
wrapper.find('#loginForm').simulate(
'submit',
{preventDefault() {}}
)
   // test to see arguments used after its been submitted 
expect(mockLoginfn.mock.calls[1][0]).toEqual(
{email: 'blah@gmail.com', password: 'cats'}
)
})

We check the mockLoginfn.mock.calls[1] because we already called our mockLoginfn in our previous test. Calls[1] gives us an array of the arguments passed in. We only used one object, so we test the first index in the array.

Summary

In my opinion the second way of testing is much simpler and would reduce code and testing time. Usually when testing our react application we can test our Redux parts separately from the components. Since our action creators and our reducers are essentially functions, we can test them like any other Javascript function. It is not necessarily important to test the whole flow from component to action to reducer and then to the store all in one test. That would be an integration test rather than a unit test for our component. If we are able to determine that each part is operating as it should through its own unit test, then we do not need anything else. Hope this helps clear up any issues you might have writing tests for connected components!

Sources:

More by Luke Pierotti

Topics of interest

More Related Stories