7
\$\begingroup\$

I'm very new to front-end/unit testing and have been having a difficult time understanding the point altogether, but I managed to push my way through testing literally everything I could possibly think of.

Anyways, if anyone has the time I'd love a review of my component and tests for said component.

Component

import React, { Component } from 'react';

import './InputForm.css';

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

    this.onFormSubmit = this.onFormSubmit.bind(this);
    this.validate = this.validate.bind(this);
    this.onInputChange = this.onInputChange.bind(this);

    this.state = {
      fields: {},
      fieldErrors: {},
    };
  }

  onInputChange(e) {
    const fields = this.state.fields;
    const newFields = {};
    newFields[e.target.name] = e.target.value;
    this.setState({
      fields: {...fields, ...newFields}
    });
  }

  validate(formData) {   
    const errors = {};
    if (!formData.name || formData.name === '' || formData.name === null) {
      errors.name = 'Please enter your name.';
    }
    return errors;
  }

  onFormSubmit(e) {
    e.preventDefault();
    const formData = this.state.fields
    const fieldErrors = this.validate(formData);

    this.setState({
      fieldErrors
    });

    if (Object.keys(fieldErrors).length) return;

    const name = this.state.fields.name;
    this.props.handleFormSubmit(name);
    this.setState({
      fields: {},
      fieldErrors: {},
    })

  }

  render() {
    return (
      <form onSubmit={(e) => this.onFormSubmit(e)}>
        <input
          type="text"
          placeholder="Name"
          value={this.state.fields.name || ''}
          name="name"
          onChange={(e) => this.onInputChange(e)}
        />
        <p className="error">
          {this.state.fieldErrors.name}
        </p>
        <input
          type="submit"
          className="btn"
          value="Submit"
        />
      </form>
      );
  }
}
InputForm.propTypes = {
  handleFormSubmit: React.PropTypes.func.isRequired,
};

export default InputForm;

Tests

// dependencies
import React from 'react';
import { shallow, mount } from 'enzyme';
import { spy } from 'sinon';

// components
import InputForm from './InputForm';

describe('<InputForm />', () => {
  let props, wrapper

  beforeEach(() => {
    props = {
      handleFormSubmit: () => {
      },
    };
    wrapper = shallow(<InputForm {...props} />);
  });

  it('should have a `<form>` element', () => {
    expect(
      wrapper.find('form').length
    ).toBe(1);
  });

  describe('<form />', () => {
    it('`<form>` element should have a onSubmit attribute', () => {
      expect(
        wrapper.props().onSubmit
      ).toBeDefined();
    });

    it('onSubmit attribute should be of type `function`', () => {
      expect(
        typeof wrapper.props().onSubmit === 'function'
      ).toBe(true);
    });

    it('`<form>` element should have an `<input />` element', () => {
      expect(
        wrapper.find('form').childAt(0).type()
      ).toBe('input');
    });

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

      it('`<input>` element should be of type `text`', () => {
        expect(
          wrapper.find('form').childAt(0).props().type
        ).toBe('text');
      });

      it('`<input>` element should have a placeholder attribute with value `Name`', () => {
        expect(
          wrapper.find('form').childAt(0).props().placeholder
        ).toBe('Name');
      });

      it('`<input>` element value should be empty', () => {
        expect(
          wrapper.find('form').childAt(0).props().value
        ).toBe('');
      });

      it('`<input>` element value should be empty', () => {
        expect(
          wrapper.find('form').childAt(0).props().value
        ).toBe('');
      });

      it('`<input>` element should have an onChange attribute', () => {
        expect(
          wrapper.find('form').childAt(0).props().onChange
        ).toBeDefined();
      });

      it('onChange attribute should be of type `function`', () => {
        expect(
          typeof wrapper.find('form').childAt(0).props().onChange === 'function'
        ).toBe(true);
      });

      it('should update the state when a value is input', () => {
        const name = 'Blerch';
        const input = wrapper.find('form').childAt(0);
        input.simulate('change', {
          target: {
            name: 'name',
            value: name,
          }
        });
        expect(
          wrapper.state().fields.name
        ).toBe(name);
      });

      it('should display an error when no value is input', () => {
        const handleFormSubmit = spy();
        wrapper = mount(<InputForm handleFormSubmit={handleFormSubmit} />);
        wrapper.find('form').simulate('submit');
        expect(
          wrapper.state().fieldErrors.name
        ).toBe('Please enter your name.');
      });

    });

    it('`<form>` element should have an `<p>` element', () => {
      expect(
        wrapper.find('form').childAt(1).type()
      ).toBe('p');
    });

    describe('<p>', () => {
      it('`<p>` element should have a className', () => {
        expect(
          wrapper.find('p').hasClass('error')
        ).toBe(true);
      });

      it('`<p>` element should be null when passed validationError: false', () => {
        expect(
          wrapper.text()
        ).toBe('');
      });

      it('`<p>` element should be `Please enter your name` when passed validationError: true', () => {
        const handleFormSubmit = spy();
        wrapper = mount(<InputForm handleFormSubmit={handleFormSubmit} />);
        wrapper.find('form').simulate('submit');
        expect(
          wrapper.text()
        ).toBe('Please enter your name.');
      });    

    });

    it('`<form>` element should have an `<input type="submit" />` element', () => {
      expect(
        wrapper.find('form').childAt(2).type()
      ).toBe('input');
    });

    describe('<input type="submit" />', () => {
      it('`<input>` element should be of type `submit`', () => {
        expect(
          wrapper.find('form').childAt(2).props().type
        ).toBe('submit');
      });

      it('`<input />` element should have a className', () => {
        expect(
          wrapper.find('form').childAt(2).hasClass('btn')
        ).toBe(true);
      });

      it('`<input />` element should have a value attribute', () => {
        expect(
          wrapper.find('form').childAt(2).props().value
        ).toBe('Submit');
      });

    });

  });

});
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

One thing that could help a lot is to realize that tests verify behavior, not implementation.

This means you shouldn't be validating these things:

  • should have a <form> element
  • <form> element should have a onSubmit attribute
  • onSubmit attribute should be of type function
  • <form> element should have an <input /> element
  • <input> element should be of type text
  • <form> element should have an <p> element
  • <p> element should have a className
  • <form> element should have an <input type="submit" /> element
  • <input> element should be of type submit
  • <input /> element should have a className
  • <input /> element should have a value attribute
  • <input> element should have an onChange attribute
  • onChange attribute should be of type function

These things are good, but the descriptions should be rewritten with a focus on the behavior, not the implementation:

  • <input> element should have a placeholder attribute with value Name
  • <input> element value should be empty (2x of these)
  • should update the state when a value is input
  • should display an error when no value is input
  • <p> element should be null when passed validationError: false
  • <p> element should be Please enter your name when passed validationError: true

Why you say? Well, why do we write tests? Many reasons, but one big one is because it gives us the ability to alter implementation without modifying behavior (refactoring). Said another way, implementations change, and often behavior should not.

Tests should only verify the external interface of the things-under-test, and the things-under-test can fall on a gradient of complexity starting from the simplest, a single function, to an object, on to an entire service or set of objects, and on to entire applications (e.g. a web app).

In this case, you're testing a component, so the external interface includes anything passed to the component (props, context, etc.), ways of interacting with the component (buttons and other input handlers), and the return value of the component (rendered element tree). Your test titled "should display an error when no value is input" is a great example of this. A pattern I follow with any tests is: given (setup), when (stimulation – aka .simulate()), then (assertion), and your test does this well.

Ultimately, you won't understand the point of testing until:

  • You find a bug – improve your tests so it never happens again
  • You need to add new features – you have assurances existing behavior won't regress
  • You build something nontrivial – begin by codifying your expectations (test stubs), then focus on one detail at a time.

... and then you'll be scared to do anything without it. In my 15 years of writing software, this fear is one of the most common reasons software projects fail (after market pressures).

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Oh, if you wrote your tests, because you want to make sure your markup never changes, use snapshot tests instead. See Jest's snapshot feature: jestjs.io, or find a counterpart for the testing tools you're using: npms.io/search?q=snapshot. Just don't use snapshots as an excuse to avoid thinking through the behavior of your component – the tests you have are still valid. \$\endgroup\$
    – Johntron
    Nov 5, 2018 at 5:58
  • \$\begingroup\$ About the point "should update the state when a value is input" wouldnt it be better to not test this and just verify this data (which is entered in input and stored in state) when the submit button is hit on the form? \$\endgroup\$
    – whyAto8
    Feb 27, 2019 at 3:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.