Test the state in parent component that was updated thru child component

Viewed 1766

I have a function called onFormSubmit in parent component. I pass this function to child component. Upon form submit in child component, onFormSubmit function inside child component is called to pass a value back to parent component. Then this onFormSubmit function does some kind of checking and based on that, updates the state in parent component.

I want to mock/stub out this ajax/api call. How can I achieve this? Or how do I write my code in such a way that this scenario is testable.

My Parent component looks like this:

class App extends React.Component {

state: { message: "" }; //I want to test the state of message

onFormSubmit = async (form) => {
    if (form.primaryEmail !== "") {
        const response = await axios.post("api/", form);
        this.setState(
            {
                message: (response.status === 200) ? "Your email has been updated." : ""
            });
    } else {
        this.setState({ message: "No Update" });
    }
}

render() {
    return (
        <div>
            <Child onSubmit={this.onFormSubmit} />
            <h4>{this.state.message}</h4>
        </div>
    );
}
}

My Child Component looks like this:

class Child extends React.Component {
state = {
    primaryEmail: "",
};

onPrimaryEmailChange = e => {
    this.setState({ primaryEmail: e.target.value });
}

onFormSubmit = e => {
    e.preventDefault();
    this.props.onSubmit(this.state); //passing the value back to parent component
}

render() {
    return (
        <form onSubmit={this.onFormSubmit}>
            <h3>Email Address</h3>
            <div>
                <input type="email" value={this.state.primaryEmail} onChange={this.onPrimaryEmailChange} />
            </div>
            <div>
                <button type="submit">Submit</button>
            </div>
        </form >
    );
}
}

My test looks like this:

test("When valid form is submitted, it should show a success message", () => {
const wrapper = mount(<App />);
wrapper.find("input").at(0).simulate("change", {
  target: {
    value: "a@b.c",
  }
});
wrapper.find('form').simulate('submit');
expect(wrapper.state('message')).toEqual('Your email has been updated.');
});

I get this error:

Expected value to equal:

"Your email has been updated."

Received:

""

1 Answers

As it happens, I ran into a similar situation earlier this week. Here's how I solved it. There may be better solutions, but this worked in my situation.

Disclaimer: I'm writing this from memory directly into the StackOverflow answer field, so it might not be 100% accurate.

First, you should mock Axios so you can have control over the API's output for your tests. You should never actually perform an HTTP request from a test case, because you're not testing your API -- you're testing how your component responds to a particular API response. My project uses create-react-app, which configures Jest to load mocks from the __mocks__ folder in the root of the project.

__mocks__/axios.js:

export default {
    // since you are only testing "axios.post()", I'm only mocking "post"
    post: jest.fn()
}

Then in your parent component's test, you can specify a mock implementation for the post function that returns a 200 response (that's the case you're testing).

__tests__/App.test.jsx:

// in Jest context, this should load from __mocks__/axios.js
import axios from "axios";

test("When valid form is submitted, it should show a success message", () => {
    // Axios itself returns a Promise, so the mock should as well
    axios.post.mockImplementationOnce(
        (url, formData) => Promise.resolve({
            status: 200
        })
    );

    const wrapper = mount(<App />);

    // Optionally, test the default state to be an empty string
    expect(wrapper.state()).toHaveProperty("message");
    expect(wrapper.state().message).toBe("");

    wrapper.find("input").at(0).simulate("change", {
        target: {
            value: "a@b.c",
        }
    });

    wrapper.find("form").simulate("submit");

    // Optionally, test if Axios was called
    expect(axios.post).toHaveBeenCalled();

    // More optionally, test if it was called with the correct email address
    expect(axios.post).toHaveBeenCalledWith(
        expect.any(),
        expect.objectContaining({ primaryEmail: "a@b.c" })
    );

    // Note that even though Axios may have been called, the asynchronous
    // Promise may not have completed yet which means the state will not
    // have been updated yet. To be safe, let's use setImmediate(), which
    // should wait for any pending Promises to complete first
    setImmediate(async () => {
        // Now we can test that the state has changed
        expect(wrapper.state().message).toBe("Your email has been updated.");
    });
});
Related