loading...

Testing Forms in React using Enzyme and Jest

austinbh profile image Austin Harlow ・3 min read

Recently I have been working on a simple blog application mainly to practice my frontend testing. Today I wanted to write about testing forms. Let's start with just my NewPost component here.

import React from 'react';
import { api } from '../services/api';

const NewPost = props => {

    const [title, setTitle] = React.useState('');
    const [content, setContent] = React.useState('');
    const [message, setMessage] = React.useState('');

    const displayMessage = jsonMessage => {
        if (jsonMessage.error) {
            let message = '';
            // Need to catch multiple errors if they exist
            for (let error in jsonMessage.error) {
                message += error + ' ' + jsonMessage.error[error] + ' '
            }
            setMessage(message)
        } else {
            setMessage('Post created successfully!')
        }
    }

    const handleChange = ev => {
        if (ev.target.name === 'title') {
            setTitle(ev.target.value)
        } else if (ev.target.name === 'content') {
            setContent(ev.target.value)
        }
    }

    const handleSubmit = ev => {
        ev.preventDefault()
        // Just using a placeholder user id since there is no login currently
        const post = {title: title, content: content, user_id: 1}
        api.posts.createPost({ post: post}).then(json => displayMessage(json))
    }

    // We want to clear out the message after 4 seconds when a post is submitted
    React.useEffect(() => {
        let timer = setTimeout(() => setMessage(''), 4000);
        return () => clearTimeout(timer);
    }, [message]);

    return (
      <div className="new-post">
        <h1>New Post</h1>
        <form className="new-post-form" onSubmit={handleSubmit}>
          <label>Title:</label>
          <input
            onChange={handleChange}
            value={title}
            type="text"
            name="title"
          />
          <label>Content:</label>
          <input
            onChange={handleChange}
            value={content}
            type="text-area"
            name="content"
          />
          <input type="submit" value="Create post" />
        </form>
        <p>{message}</p>
      </div>
    );
}

export default NewPost;

This form is fairly simple all we have is a title and the content for our post. In order to be able to test React's useState function we are not naming the import but just calling the useState method on our React import.

const [title, setTitle] = React.useState('');

This will allow us to test the state calls when we update the title or content fields on our form. To get started with our tests let's add all of our imports and configure our adapter.


import React from "react";
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import NewPost from "../components/NewPost";

Enzyme.configure({adapter: new Adapter() });

In a similar manner we are also going to write a describe block for our component to contain all of our form tests.

describe("<NewPost />", () => {
    let wrapper;
    const setState = jest.fn();
    const useStateSpy = jest.spyOn(React, "useState")
    useStateSpy.mockImplementation((init) => [init, setState]);

    beforeEach(() => {
        wrapper = Enzyme.mount(Enzyme.shallow(<NewPost />).get(0))
    });

    afterEach(() => {
        jest.clearAllMocks();
    });

First things first we are initializing a wrapper variable that we will use the mount function available through Enzyme to have a copy of our component. Then we create a state spy so that we can check that React's useState function is called. Finally, we write our beforeEach and afterEach functions to mount our component and then clear all jest mocks.

Now let's get into the meat of testing our useState calls.

    describe("Title input", () => {
        it("Should capture title correctly onChange", () => {
            const title = wrapper.find("input").at(0);
            title.instance().value = "Test";
            title.simulate("change");
            expect(setState).toHaveBeenCalledWith("Test");
        });
    });

    describe("Content input", () => {
        it("Should capture content correctly onChange", () => {
            const content = wrapper.find("input").at(1);
            content.instance().value = "Testing";
            content.simulate("change");
            expect(setState).toHaveBeenCalledWith("Testing");
        });
    });

This first describe block is testing our title input which we can see by finding the first input. From here we set it's value to "Test" and then initiate a change action. We want to check that our setState function is called with this title. The same pattern follows for our content input test. We are checking that our setState function is being called with the updated input of "Testing".

Posted on by:

austinbh profile

Austin Harlow

@austinbh

Full Stack Software Engineer from Seattle. Always learning and trying new things.

Discussion

markdown guide
 

I'd also recommend not to inspect setState for testing.
I know that this is only an example, but in my personal experience I'd also recommend using the non-controlled version of the inputs when possible, and this seems to be the case.
That way all the setState become unnecessary.
The onSubmit event on the form will receive the event, which contains the values of the inputs. You can get rid of a lot of boilerplate code, that way 😉

 

I came here to call you a liar... surely it's not that simple. Even the React docs focus almost exclusively on controlled components, and have a small section that suggests that to use uncontrolled components, you need refs. The belief is so prolific that we have some rather monstrous Rube Goldberg machines to "help" with this (Looking at you, psychopaths who made redux-form).

But he's right folks. 9/10 times this will do just fine:

const handleSubmit = (event) => {
  event.preventDefault()
  const formData = new FormData(event.target)
  doSomethingWithFormData(Object.fromEntries(formData))
}

Thanks for reminding me to always question convention. ;-)

 

Testing component by spying on useState or any internal methods can be considered as anti-pattern because all those tests rely on implementation details. It leads to massive tests rewriting in case of code refactoring
The better solution is to test component as a black box.
All benefits of this approach are described here

 

Hi Austin, you can also check our uniforms.tools - A React library for building forms from any schema.
Best!