21

I have the following React component:

class Form extends React.Component {

   constructor(props) {
      super(props);
      this.state = this._createEmptyTodo();
   }

   render() {

      this.i18n = this.context;

      return (
         <div className="form">
            <form onSubmit={this._handleSubmit.bind(this)}>

               <input
                  placeholder={this.i18n.placeholders.addTitle}
                  type="text"
                  value={this.state.title}
                  onChange={this._handleTitleChange.bind(this)}></input>

               <textarea
                  placeholder={this.i18n.placeholders.addDescription}
                  value={this.state.description}
                  onChange={this._handleDescriptionChange.bind(this)}></textarea>

               <button>{this.i18n.buttons.submit}</button>
            </form>
         </div>
      );
   }

   _handleTitleChange(e) {
      this.setState({
         title: e.target.value
      });
   }

   _handleDescriptionChange(e) {
      this.setState({
         description: e.target.value
      });
   }

   _handleSubmit(e) {

      e.preventDefault();

      var todo = {
         date: new Date().getTime(),
         title: this.state.title.trim(),
         description: this.state.description.trim(),
         done: false
      };

      if (!todo.title) {
         alert(this.i18n.errors.title);
         return;
      }

      if (!todo.description) {
         alert(this.i18n.errors.description);
         return;
      }

      this.props.showSpinner();
      this.props.actions.addTodo(todo);
      this.setState(this._createEmptyTodo());
   }

   _createEmptyTodo() {
      return {
         "pkey": null,
         "title": "",
         "description": ""
      };
   }
}

And the related test:

const i18nContext = React.createContext();
Form.contextType = i18nContext;

    describe('The <Form> component', () => {

       var wrapper;
       var showSpinner;
       var actions = {}

       beforeEach(() => {
          showSpinner = jest.fn();
          actions.addTodo = jest.fn();
          wrapper = mount(<i18nContext.Provider value={i18n["en"]}>
             <Form
                showModalPanel={showSpinner}
                actions={actions} />
          </i18nContext.Provider>);
       });

       test("validate its input", () => {
          window.alert = jest.fn();
          wrapper.find("button").simulate("click");
          expect(window.alert.mock.calls.length).toBe(1);//<<< this FAILS!
       });
    });

This form, when the button gets clicked, it simply alerts a message using alert.

Now when I run the test I get this:

expect(received).toBe(expected) // Object.is equality

Expected: 1
Received: 0

Which is a failure because the mock does not get called apparently. But I promise you that the form component does alert a message when clicking on its button.

I suspect that, for some reasons, the mocked window.alert does not get used by the Form component when the click is performed programmatically using enzyme.

Anyone?

brass monkey
  • 5,841
  • 10
  • 36
  • 61
nourdine
  • 7,407
  • 10
  • 46
  • 58
  • Please, provide https://stackoverflow.com/help/mcve . The question should include the code you're testing. If you wanted to be sure that click is performed, a proper way would be to spy on click handler. – Estus Flask Dec 04 '18 at 11:01
  • I have added the complete code – nourdine Dec 04 '18 at 12:38
  • 1
    I'm not sure at all that Enzyme triggers form submit event when button click is simulated. You're using functional testing which is faulty by design exactly because of the situation you have; once a test failed, you don't know what went wrong. Consider using proper unit testing, especially since Enzyme allows that. If you want to test how handleSubmit works, call it directly and mock component state if necessary. – Estus Flask Dec 04 '18 at 12:49
  • @estus I see. Anyway what do you mean when u say "Enzyme allows that"? Can you point me towards some examples? – nourdine Dec 04 '18 at 13:18
  • 1
    This specific to testing strategy rather than Enzyme. You can access class instance with `instance()`. That's what you need for isolated unit test. I've tried to provide an example. – Estus Flask Dec 04 '18 at 13:45

2 Answers2

31

In Jest configuration with JSDOM global.window === global, so it can be mocked on window.

It's preferable to mock it like

jest.spyOn(window, 'alert').mockImplementation(() => {});

because window.alert = jest.fn() contaminates other tests in this suite.

The problem with blackbox testing is that troubleshooting is harder, also relying on the behaviour that expected from real DOM may cause problems because Enzyme doesn't necessary support this behaviour. It's unknown whether the actual problem, handleSubmit was called or not, that alert mock wasn't called is just an evidence that something went wrong.

In this case click event on a button won't cause submit event on parent form because Enzyme doesn't support that by design.

A proper unit-testing strategy is to set up spies or mocks for all units except tested one, which is submit event handler. It usually involves shallow instead of mount.

It likely should be:

  jest.spyOn(window, 'alert').mockImplementation(() => {});
  const formWrapper = wrapper.find(Form).dive();
  jest.spyOn(formWrapper.instance(), '_handleSubmit');
  formWrapper.find("form").simulate("submit");
  expect(formWrapper.instance()._handleSubmit).toBeCalled();
  expect(window.alert).toBeCalledWith(...);

State should be changed directly with formWrapper.setState instead of DOM events simulation.

A more isolated unit test would be to assert that form was provided with expected onSubmit prop and call formWrapper.instance()._handleSubmit(...) directly.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
7

Instead of window, you can use global.

global.alert = jest.fn();

This is because browsers use the window name, while nodejs use the global name.

Errorname
  • 2,228
  • 13
  • 23