djstozza
djstozza

Reputation: 91

Jest - Mocking a function call within the handleSubmit of a form

I am trying to write a test that mocks the calling of a function within the handleSubmit of a form, however, I am unable to show that the function has been called.

The form is as follows:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import signUp from '../../actions/users/sign_up';
import PropTypes from 'prop-types';

class Signup extends Component {
  constructor (props) {
    super(props);

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.showError = this.showError.bind(this);
  }

  handleChange(event) {
    const target = event.target;

    this.setState({ [ target.name ]: target.value });
  }

  handleSubmit(event) {
    event.preventDefault();
    this.props.signUp(this.state);
  }

  showError(type) {
    if (this.state && this.state.error && this.state.error.data.errors[ type ]) {
      return this.state.error.data.errors[ type ][ 0 ];
    }
  }

  componentDidUpdate (prevProps, prevState) {
    const props = this.props;

    if (prevProps === props) {
      return;
    }

    this.setState({
      ...props,
    });
  }

  render () {
    return (
        <div className='container-fluid'>
            <div className='row'>
                <div className='col col-md-6 offset-md-3 col-sm-12 col-12'>
                    <div className='card'>
                        <div className='card-header'>
                            <h4>Sign Up</h4>
                        </div>
                        <div className='card-body'>
                            <form onSubmit={ this.handleSubmit } >
                                <div className="form-row">
                                    <div className="form-group col-md-12">
                                        <label htmlFor="email">Email</label>
                                        <input
                        type="email"
                        name="email"
                        className={ `form-control ${ this.showError('email') ? 'is-invalid' : '' }` }
                        id="email"
                        placeholder="Email"
                        onChange={ this.handleChange }
                      />
                                        <div className="invalid-feedback">
                                            { this.showError('email') }
                                        </div>
                                    </div>
                                </div>
                                <div className="form-row">
                                    <div className="form-group col-md-12">
                                        <label htmlFor="username">Username</label>
                                        <input
                        type="text"
                        name="username"
                        className={ `form-control ${ this.showError('username') ? 'is-invalid' : '' }` }
                        id="username"
                        placeholder="Username"
                        onChange={ this.handleChange }
                      />
                                        <div className="invalid-feedback">
                                            { this.showError('username') }
                                        </div>
                                    </div>
                                </div>
                                <div className="form-row">
                                    <div className="form-group col-md-12">
                                        <label htmlFor="password">Password</label>
                                        <input
                          type="password"
                          name="password"
                          className={ `form-control ${ this.showError('password') ? 'is-invalid' : '' }` }
                          id="password"
                          placeholder="Password"
                          onChange={ this.handleChange }
                        />
                                        <div className="invalid-feedback">
                                            { this.showError('password') }
                                        </div>
                                    </div>
                                    <button type="submit" className="btn btn-primary">Sign Up</button>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    )
  }
}

function mapStateToProps (state) {
  return {
    email: state.UsersReducer.email,
    username: state.UsersReducer.username,
    password: state.UsersReducer.password,
    error: state.UsersReducer.error,
  }
}

function mapDispatchToProps (dispatch) {
  return bindActionCreators({
    signUp: signUp,
  }, dispatch);
}

Signup.propTypes = {
  email: PropTypes.string,
  username: PropTypes.string,
  password: PropTypes.string,
  signUp: PropTypes.func.isRequired
}

export default connect(mapStateToProps, mapDispatchToProps)(Signup);

The signUp action looks like this:

import { SIGN_UP, SHOW_USER_ERRORS } from '../types';
import axios from 'axios';
import { API_ROOT,  setLocalStorageHeader } from './../../api-config';
import { push } from 'react-router-redux';

export default function signUp (params) {
  return dispatch => {
    axios.post(`${ API_ROOT }/auth.json`, params).then(res => {
      setLocalStorageHeader(res);
      dispatch(push('/profile'));
      dispatch(signUpAsync(res.data));
    }).catch(error => {
      dispatch({ type: SHOW_USER_ERRORS, payload: { error: error.response } });
    });
  }
}

function signUpAsync (data) {
  return {
    type: SIGN_UP,
    payload: data
  };
}

I am trying to simulate the fact that the form will be submitted with the values obtained from the form inputs, which are in the form's state (email, username and password).

The test I currently have is:

import React from 'react';
import { shallow, mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { bindActionCreators } from 'redux';
import thunk from 'redux-thunk';

import Signup from '../../../components/users/signup';
import UsersReducer from '../../../reducers/reducer_users';

describe('<Signup />', () => {
  describe('render()', () => {
    test('submits the form data',  async () => {
      const mockStore = configureStore([thunk]);

      const initialState = {
        UsersReducer: {
          email: '',
          username: '',
          password: '',
        },
      };

      const store = mockStore(initialState);
      const dispatchMock = jest.spyOn(store, 'dispatch');

      const signUp = jest.fn();

      const wrapper = shallow(<Signup store={store} signUp={signUp} />);
      const component = wrapper.dive();

      component.find('#email').simulate(
        'change', {
          target: {
            name: 'email', value: '[email protected]'
          }
        }
      );

      component.find('#email').simulate(
        'change', {
          target: {
            name: 'username', value: 'foo'
          }
        }
      );

      component.find('#password').simulate(
        'change', {
          target: {
            name: 'password',
            value: '1234567',
          }
        }
      )

      component.find('form').simulate(
        'submit', {
          preventDefault() {}
        }
      )

      expect(dispatchMock).toHaveBeenCalled();

      expect(signUp).toHaveBeenCalledWith({
        email: '[email protected]',
        username: 'foo',
        password: '12345678'
      });
    });
  });
});

But I keep getting the following error no matter what I try.

Expected mock function to have been called with:
  [{"email": "[email protected]", "password": "12345678", "username": "foo"}]
But it was not called.

I think it's due to the fact that signUp isn't being mocked properly in shallow(<Signup store={store} signUp={signUp} />) because when I do console.log(wrapper.props()) I get:

{
...
signUp: [Function],
...
}

rather than an indication that it's a mocked function:

{ [Function: mockConstructor]
   _isMockFunction: true,
...
}

I know that the signUp action is being called by the dispatch of the test is passing. I can also see the params in the signUp action when I add a console.log(params) into it.

Any assistance would be greatly appreciated.

Upvotes: 1

Views: 5457

Answers (2)

djstozza
djstozza

Reputation: 91

So, after a lot of trial and error, the solution was to mock the action call itself which was done by adding import * as signUp from '../../../actions/users/sign_up'; and mocking it with const signUpActionMock = jest.spyOn(signUp, 'default');

The test now looks like this:

import React from 'react';
import { shallow } from 'enzyme';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';

import Signup from '../../../components/users/signup';
import UsersReducer from '../../../reducers/reducer_users';

// Turns out this import allowed the signUp action to be mocked
import * as signUp from '../../../actions/users/sign_up';

describe('<Signup />', () => {
  describe('render()', () => {
    test('submits the form data', () => {
      const middlewares = [thunk]

      // Mock the signUp action call
      const signUpActionMock = jest.spyOn(signUp, 'default');

      const mockStore = configureStore(middlewares);

      const initialState = {
        UsersReducer: {
          email: '',
          username: '',
          password: '',
        },
      };

      const store = mockStore(initialState);

      const wrapper = shallow(<Signup store={store} />);
      const component = wrapper.dive();

      component.find('#email').simulate(
        'change', {
          target: {
            name: 'email', value: '[email protected]'
          }
        }
      );

      component.find('#email').simulate(
        'change', {
          target: {
            name: 'username', value: 'foo'
          }
        }
      );

      component.find('#password').simulate(
        'change', {
          target: {
            name: 'password',
            value: '12345678',
          }
        }
      );

      component.find('form').simulate(
        'submit', {
          preventDefault() {}
        }
      );

      expect(signUpActionMock).toHaveBeenCalledWith({
        email: '[email protected]',
        username: 'foo',
        password: '12345678'
      });
    });
  });
});

Upvotes: 0

Andreas K&#246;berle
Andreas K&#246;berle

Reputation: 110932

Your add signUp in the mapDispatchToProps when adding redux to the view.

As you use redux-mock-store you can access all actions that were called by store.getActions() So in your case, instead of passing a signUp as spy which will be overwritten by mapDispatchToProps, it could look like this:

const signUpCall = store.getActions()[0]

expect(signUpCall).toHaveBeenCalledWith({
        email: '[email protected]',
        username: 'foo',
        password: '12345678'
      });

Upvotes: 1

Related Questions