React/Redux Testing w/ Enzyme

2.6k views Asked by At

I'm learning how to test React/Redux components using enzyme. The component takes app-level state as props. When I run the test, I get the errors:

Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components).

TypeError: Cannot read property 'contextTypes' of undefined

with my console.log of wrapper in the below test file logging as undefined.

I know that there's something wrong with my setup here, and have spent a couple hours trying to figure this out. Can anybody see anything obvious in the way that I'm importing and trying to use the component? I can't figure out why it is undefined. Thanks in advance for any help or insight!

BackendDisplay.js

import React from 'react';
import { connect } from 'react-redux';
import moment from 'moment';

var BackendDisplay = React.createClass({

  render() {

    const { username, node_version, app_path, timestamp } = this.props.loginState;
    const dateTime = moment(timestamp).format('MMMM Do YYYY, h:mm:ss a');

    return (
      <div>
        <h1>Welcome, {username}!</h1>
        <p><span className="bold">Node Version:</span> {node_version}</p>
        <p><span className="bold">Application Path:</span> {app_path}</p>
        <p><span className="bold">Date/Time:</span> {dateTime}</p>
      </div>
    );
  }
});

const mapStateToProps = function(store) {
  return store;
}

module.exports = connect(mapStateToProps)(BackendDisplay);

BackendDisplay.test.js

'use strict';

import React from 'react';
import {shallow} from 'enzyme';
import { connect } from 'react-redux';
import { BackendDisplay } from '../components/BackendDisplay';

describe('<BackendDisplay />', () => {

  it('Correctly displays username, node_version, app_path, and timestamp', () => {

    const wrapper = shallow(<BackendDisplay />);
    console.log(wrapper);

  });

});

Edited after changes: BackendDisplay.js

import React from 'react';
import { connect } from 'react-redux';
import moment from 'moment';

var BackendDisplay = React.createClass({

  render() {

    const { username, node_version, app_path, timestamp } = this.props.loginState;
    const dateTime = moment(timestamp).format('MMMM Do YYYY, h:mm:ss a');

    return (
      <div>
        <h1>Welcome, {username}!</h1>
        <p><span className="bold">Node Version:</span> {node_version}</p>
        <p><span className="bold">Application Path:</span> {app_path}</p>
        <p><span className="bold">Date/Time:</span> {dateTime}</p>
      </div>
    );
  }
});

const mapStateToProps = function(store) {
  return store;
}

// module.exports = connect(mapStateToProps)(BackendDisplay);
export default connect(mapStateToProps)(BackendDisplay);

BackendDisplay.test.js

'use strict';

import React from 'react';
import {shallow} from 'enzyme';
import { connect } from 'react-redux';
import store from '../store';
import { Provider } from 'react-redux';
import ConnectedBackendDisplay, {BackendDisplay} from '../components/BackendDisplay';

describe('<BackendDisplay />', () => {

  it('Correctly displays username, node_version, app_path, and timestamp', () => {

    const wrapper = shallow(
      <Provider store={store}>
        <BackendDisplay />
      </Provider>
    );

    console.log(wrapper.find(BackendDisplay));
    expect(wrapper.find(BackendDisplay).length).to.equal(1);

  });

});

Error message: TypeError: Enzyme::Selector expects a string, object, or Component Constructor

1

There are 1 answers

10
therewillbecode On BEST ANSWER

Your BackendDisplay is a container component and it is connected to the Redux store through the use of the connect api.

You should export the undecorated component for testing purposes. Since it is undecorated this exported component will not be wrapped with react-redux's Connect component.

var BackendDisplay = React.createClass({

  render() {

    const { username, node_version, app_path, timestamp } = this.props.loginState;
    const dateTime = moment(timestamp).format('MMMM Do YYYY, h:mm:ss a');

    return (
      <div>
        <h1>Welcome, {username}!</h1>
        <p><span className="bold">Node Version:</span> {node_version}</p>
        <p><span className="bold">Application Path:</span> {app_path}</p>
        <p><span className="bold">Date/Time:</span> {dateTime}</p>
      </div>
    );
  }
});

Then you can import it as follows to make the test work

import {BackendDisplay} from 'BackendDisplay'

As a bonus you can also export the decorated BackendDisplay component by changing the following line

module.exports = connect(mapStateToProps)(BackendDisplay);

to

 export default connect(mapStateToProps)(BackendDisplay);

This is how to import both the decorated and undecorated components

import ConnectedBackendDisplay, {BackendDisplay} from 'BackendDisplay'  

ConnectedBackendDisplay refers to the decorated component which is exported through the unnamed export (export default BackendDisplay).

We just give it this name so that its clear it is wrapped in a connect component.

I have updated the following component to use export default which gives an unnamed export.

BackendDisplay

import React from 'react';
import { connect } from 'react-redux';
import moment from 'moment';

export const BackendDisplay = React.createClass({

  render() {

    const { username, node_version, app_path, timestamp } = this.props;
    // removed reference to this.props.loginState

    const dateTime = moment(timestamp).format('MMMM Do YYYY, h:mm:ss a');

    return (
      <div>
        <h1>Welcome, {username}!</h1>
        <p><span className="bold">Node Version:</span> {node_version}</p>
        <p><span className="bold">Application Path:</span> {app_path}</p>
        <p><span className="bold">Date/Time:</span> {dateTime}</p>
      </div>
    );
  }
});

const mapStateToProps = function(store) {
  return store;
}

export default connect(mapStateToProps)(BackendDisplay);

Here is the test suite to demonstrate testing the above component both as a decorated and undecorated component with enzyme.

I am using the chai library to make test assertions easier. The jsdom library is also being used to create a DOM environment so we can test components using Enzyme's mount function which fully renders components.

test

'use strict';
import React from 'react';
import jsdom from 'jsdom'
import { expect } from 'chai'
import { shallow , mount} from 'enzyme';
import { Provider } from 'react-redux';
import ConnectedBackendDisplay, // decorated component
   {BackendDisplay} from 'app/components/BackendDisplay';  // undecorated component
// for mocking a store to test the decorated component
import configureMockStore from 'redux-mock-store'; 

// create a fake DOM environment so that we can use Enzyme's mount to
// test decorated components
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>')
global.document = doc
global.window = doc.defaultView


describe.only('<BackendDisplay />', () => {

    it('undecorated component correctly displays username', () => {
        // define the prop we want to pass in
        const username = 'Foo'
        // render the component with the prop
        const wrapper = mount(<BackendDisplay username={username} />);
        // test that the text of the first <p> equals username prop that we passed in       
        expect(wrapper.find('h1').first().text()).to.equal(username);
   });

    it('decorated component correctly displays username', () => {
        // define the prop we want to pass in
        const username = 'Foo'
        const initialState = { }
        // create our mock store with an empyty initial state
        const store = configureMockStore(initialState)

        // render the component with the mockStore
        const wrapper = shallow(<Provider store={store}>
                                <ConnectedBackendDisplay username={username}/>
                              </Provider>);

        // test that the text of the first <p> equals username prop that we passed in       
        expect(wrapper.find('h1').first().text()).to.equal(username);
   });
});