Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot read property 'contextTypes' of undefined Unit Testing enzyme

I'm trying unit testing in my react using redux app. So I need to test connected components unfortunately i got this error:

Cannot read property 'contextTypes' of undefined I use enzyme in unit testing Here is my component:

    import React from 'react';
    import TextFieldGroup from '../common/TextFieldGroup';
    import validateInput from '../../server/validations/login';
    import { connect } from 'react-redux';
    import { login } from '../../actions/authActions';

    class LoginForm extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          username: '',
          password: '',
          errors: {},
          isLoading: false
        };

        this.onSubmit = this.onSubmit.bind(this);
        this.onChange = this.onChange.bind(this);
      }

      isValid() {
        const { errors, isValid } = validateInput(this.state);

        if (!isValid) {
          this.setState({ errors });
        }

        return isValid;
      }

      onSubmit(e) {
        e.preventDefault();
        if (this.isValid()) {
          this.setState({ errors: {}, isLoading: true });
          this.props.login(this.state).then(
            (res) => this.context.router.push('/'),
            (err) => this.setState({ errors: err.response.data.errors, isLoading: false })
          );
        }
      }

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

      render() {
        const { errors, username, password, isLoading } = this.state;

        return (
          <form onSubmit={this.onSubmit}>
            <h1>Login</h1>

            { errors.form && <div className="alert alert-danger">{errors.form}</div> }

            <TextFieldGroup
              field="username"
              label="Username"
              value={username}
              error={errors.username}
              onChange={this.onChange}
            />

            <TextFieldGroup
              field="password"
              label="Password"
              value={password}
              error={errors.password}
              onChange={this.onChange}
              type="password"
            />

            <div className="form-group"><button className="btn btn-primary" disabled={isLoading}>Login</button></div>
          </form>
        );
      }
    }

    LoginForm.propTypes = {
      login: React.PropTypes.func.isRequired
    }

    LoginForm.contextTypes = {
      router: React.PropTypes.object.isRequired
    }

    export default connect(null, { login })(LoginForm);

Here is my test:

    import React from 'react';
    import { mount, shallow } from 'enzyme';
    import {expect} from 'chai';
    import sinon from 'sinon';
    import { connect } from 'react-redux'

    import { Login } from '../../js/react/components/login/LoginForm';

    describe('<Login />', function () {
      it('should have an input for the username', function () {
        const wrapper = shallow(<Login />);
        expect(wrapper.find('input[name=username]')).to.have.length(1);
      });

      it('should have an input for the password', function () {
        const wrapper = shallow(<Login />);
        expect(wrapper.find('input[name=password]')).to.have.length(1);
      });

      it('should have a button', function () {
        const wrapper = shallow(<Login />);
        expect(wrapper.find('button')).to.have.length(1);
      });

      it('simulates click events', () => {
        const onButtonClick = sinon.spy();
        const wrapper = shallow(
          <Login onButtonClick={onButtonClick} />
        );
        wrapper.find('button').simulate('click');
        expect(onButtonClick).to.have.property('callCount', 1);
      });

    });

Suggestions and answers are highly appreciated :)

like image 705
Frank Mendez Avatar asked Jan 31 '17 06:01

Frank Mendez


2 Answers

Testing a decorated component

To test the component directly you need to export just the component itself function without passing it into Connect.

Currently, in your test you are importing the wrapper component returned by connect(), and not the LoginForm component itself. This is fine if you want to test the interaction of the LoginForm component with Redux. If you are not testing the component in isolation then this method of exporting and importing the component is fine. Howver you need to remember to wrap the component in your test with a a component which is created specifically for this unit test. Let's now look at this case where we are testing a connected component and explain why we are wrapping it in in our unit tests.

The relationship between Provider and Connect components in react-redux

The react-redux library gives us a Provider component. The purpose of Provider is to allow any of its child components to access the Redux store when wrapped in a Connect component. This symbiotic relationship between Provider and Connect allow any component which is wrapped in the Connect component to access the Redux store via React's context feature.

Testing the connected component

Remember that a connected component is a component that is wrapped in a Connect component and this wrapping gives our component access to the Redux store? For this reason we need to create a mock store in our test file since we need a store in order to test how the component interacts with it.

Giving our Connected Component a store with Provider in tests

However Connect doesn't know how to magically access the store. It needs to be nested (wrapped) in a component. The Provider component gives our component wrapped in Connect access to the Store by hooking into Provider via React's context api.

As we can see Provider takes a prop consisting of the Redux store:

ReactDOM.render(
      <Provider store={store}>
        <MyRootComponent />
      </Provider>,
      rootEl
    )

So to test the connected component you need to wrap it with Provider. Remember Provider takes the Redux store object as a prop. For our tests we can use the redux-mock-store library which helps us set up a mock store and gives us some methods on the store to track which actions have been dispatched if we need them.

import { Provider } from 'react-redux'
import { mount } from 'enzyme'
import chai, {expect} from 'chai'
import chaiEnzyme from 'chai-enzyme'
import configureMockStore from 'redux-mock-store'

chai.use(chaiEnzyme())

import ConnectedLoginForm, from '../app.js'
const mockStore = configureMockStore(middlewares)

describe.only('<LoginForm Component />', () => {
it('LoginForm should pass a given prop to its child component' () => {
    const store = mockStore(initialState)
    const wrapper = mount(
      <Provider store={store}>
        <ConnectedLoginForm />
      </Provider>
    )
    expect(wrapper.type()).to.equal('div')
  })
})

But sometimes you want to test just the rendering of the component, without a Redux store. Lets look at this case where we want to test the rendering of the unconnected LoginForm component in isolation.

Testing the unconnected component

So currently you are testing the new component that is created by wrapping the original LoginForm component with Connect.

In order to test the original component itself without connect, create a second export declaration solely for the LoginForm component.

import { connect } from 'react-redux'

// Use named export for unconnected component (for tests)
export class App extends Component { /* ... */ }

// Use default export for the connected component (for app)
export default connect(mapStateToProps)(App)

You can now test rendering of the component without the Redux store.

Importing the components in tests

Remember when you export the component in this way -

  • Default exports for importing connected component
  • Named exports for importing unconnected component

Now in your test file import the undecorated LoginForm component like this:

// Note the curly braces: grab the named export instead of default export
import { LoginForm } from './App'

Or import both undocarated and decorated (connected) components:

import ConnectedLoginForm, { LoginForm } from './App'

If you want to test how the LoginForm component interacts with your Redux store you must

like image 185
therewillbecode Avatar answered Oct 04 '22 01:10

therewillbecode


You didn't export your Login component. Meaning:

class LoginForm extends React.Component { ... -> export class LoginForm extends React.Component { ...

like image 24
Kim Avatar answered Oct 04 '22 02:10

Kim