Enzyme: Testing a component wrapped inside a router and redux-form

Created on 22 May 2018  路  6Comments  路  Source: enzymejs/enzyme

Describe the bug
I'm trying to unit test a react component (called SayConfig) which has nested components and which is wrapped inside a router and redux form. However, due to the router and redux form, I have to call .dive() several times in order to access to the component I want to test. But when trying to .dive() on , I got the following error:

Invariant Violation: You should not use or withRouter() outside a Router

What I want to do is to make unit tests on mapStateToProps() and mapDispatchToProps().

Here is the code:

_SayConfigComponent.jsx (contains JSX definition of the component)_

export default ({
  saveTemplate,
  handleSubmit,
  templates,
  makeTemplateEditable,
  onChangeTemplateText,
  finishTemplateEditionAction,
  deleteTemplate,
  configUI,
  editTemplate,
  closePanel,
}:
FullProps) => (
  <Panel className="SayConfigPanel">
    <Panel.Heading>
      <Glyphicon glyph="remove" onClick={() => closePanel()} />
      <Panel.Title componentClass="h3">My Title</Panel.Title>
    </Panel.Heading>
    <Panel.Body>
      {configUI.showEditionForm ?
        <form onSubmit={
          handleSubmit(data => editTemplate(data)}
        >
          <ControlLabel>Edit an existing template</ControlLabel>
          <Field
            name="newTemplate"
            component={EditTemplateField}
            onChange={onChangeTemplateText}
            text={configUI.templateToEdit.text}
          />
          <Button bsStyle="success" className="pull-right" type="submit">Edit</Button>
          <Button bsStyle="danger" className="pull-right" onClick={finishTemplateEditionAction}>Cancel</Button>
        </form>
        :
        <form onSubmit={handleSubmit(data => saveTemplate(data))} >
          <ControlLabel>Add a template</ControlLabel>
          <Field
            name="template"
            component={AddTemplateField}
          />
          <Button bsStyle="primary" className="pull-right" type="submit">Save</Button>
        </form>
      }
      <Templates
        templates={templates}
        makeTemplateEditable={makeTemplateEditable}
        deleteTemplate={deleteTemplate}
      />
    </Panel.Body>
  </Panel>
);

_index.js (contains mapStateToProps and mapDispatchToProps and connects the component to the router and redux form)_

import SayConfigComponent from './component';
const mapStateToProps = (state: State, { bot, domain, nodeId }): StateProps => {
  // some lines use bot, domain and nodeId
  const templates: Array<string> = getTemplatesFromSomewhere();

  return {
    templates,
    configUI: state.ui.config,
  };
};

const mapDispatchToProps = (dispatch: Function, { bot, domain }): DispatchProps => ({
  saveTemplate: (data) => {
    // dispatch some actions
  },
  makeTemplateEditable: (index, text) => {
   // dispatch some actions
  },
  editTemplate: (data) => {
   // dispatch some actions
  },
  deleteTemplate: (index) => {
   // dispatch some actions
  },
  finishTemplateEditionAction: () => {
   // dispatch some actions
  },
  onChangeTemplateText: (event) => {
   // dispatch some actions
  },
  closeSayConfig: () => {
   // dispatch some actions
  },
});

const connectedWrapper = withRouter(connect(
  mapStateToProps,
  mapDispatchToProps,
)(SayConfigComponent));

export default reduxForm({
  form: 'sayConfigTemplateForm',
  fields: ['template', 'newTemplate'],
})(connectedWrapper);

_index.spec.js (supposed to contain the test for mapStateToProps and mapDispatchToProps)_

import React from 'react';
import { shallow } from 'enzyme';
import configureMockStore from 'redux-mock-store';
import SayConfigIndex from './index';

const mockStore = configureMockStore();

describe('SayConfigComponent', () => {
  let wrapper;
  let store;

  beforeEach(() => {
    const initialState = {
      // some params for initial state
    };

    store = mockStore(initialState);

    wrapper = shallow(<SayConfigIndex bot="test" domain="test" nodeId="n2" />, { context: { store } }).dive().dive().dive().dive();
   // returns Invariant Violation: You should not use <Route> or withRouter() outside a <Router>
   // If I remove one dive(), i get the following component : <Route render={[Function: render]} />
  });

  it('MapStateToProps > it should properly return nodeId, say templates of selected node and sayConfigUI', () => {
    console.log('debug: ', wrapper.debug());
    console.log('selector:', wrapper.find('.SayConfigPanel'));
  });

});

My question is : how can I test such a component and what are the best practices?

question

Most helpful comment

Hi @willemotlucas there is a good explanation in redux docs, and you can find it by this link.

In short if you want to test a component which is wrapped with any wrapper you can export an original component and test it instead of a wrapped component.

The same can be applied for mapStateToProps and mapDispatchToProps in your case. You can export them as well and cover by unit tests.

// your component file
class YourComponent extends React.Component {
  // component logic here
}

export const mapStateToProps = () => { /* your logic here */ };
export { YourComponent };
export default connect(mapStateToProps)(YourComponent);
// your test file
import { mapStateToProps, YourComponent } from './your/component'

describe('your component description', () => {
  describe('YourComponent', () => {
    test('test description', () => {
      // your test here
    });
  });

  describe('mapStateToProps', () => {
    test('should return a mapped state to props object', () => {
      // your test here
    });
  });  
});  

In case if you want to test real user scenarios unit tests are not a solution for it, because the whole idea of them is to test some unit of logic not a whole user case. In order to cover real user scenarios e2e tests would work better. You can find more info by this link or you can google it :)

Next image illustrates a case when unit tests (doors can be closed/opened) are passing but application does not work as expected:

1_3nauvsj75ir0unsvjf_8pg

All 6 comments

Hi @willemotlucas there is a good explanation in redux docs, and you can find it by this link.

In short if you want to test a component which is wrapped with any wrapper you can export an original component and test it instead of a wrapped component.

The same can be applied for mapStateToProps and mapDispatchToProps in your case. You can export them as well and cover by unit tests.

// your component file
class YourComponent extends React.Component {
  // component logic here
}

export const mapStateToProps = () => { /* your logic here */ };
export { YourComponent };
export default connect(mapStateToProps)(YourComponent);
// your test file
import { mapStateToProps, YourComponent } from './your/component'

describe('your component description', () => {
  describe('YourComponent', () => {
    test('test description', () => {
      // your test here
    });
  });

  describe('mapStateToProps', () => {
    test('should return a mapped state to props object', () => {
      // your test here
    });
  });  
});  

In case if you want to test real user scenarios unit tests are not a solution for it, because the whole idea of them is to test some unit of logic not a whole user case. In order to cover real user scenarios e2e tests would work better. You can find more info by this link or you can google it :)

Next image illustrates a case when unit tests (doors can be closed/opened) are passing but application does not work as expected:

1_3nauvsj75ir0unsvjf_8pg

This seems answered.

Thanks @mykhailo-riabokon, that was a lifesaver

Thanks @mykhailo-riabokon !!!

connect

The above example doesn't explain a component wrapped inside withRouter and then connect.

Enzyme definitely lags behind this area.

@maxpaynestory that's two HOCs, so you'd need two .dive()s. If there's something more, please file a new issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

potapovDim picture potapovDim  路  3Comments

aweary picture aweary  路  3Comments

mattkauffman23 picture mattkauffman23  路  3Comments

heikkimu picture heikkimu  路  3Comments

AdamYahid picture AdamYahid  路  3Comments