Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock axios for formik submit?

The problem I'm running into is not being able to test if the axios put method was called after clicking the submit button.

I have the following in my __mocks__/axios.js:

const defaultResponse = { data: {} };

const __mock = {
  reset() {
    Object.assign(__mock.instance, {
      get: jest.fn(() => Promise.resolve(defaultResponse)),
      put: jest.fn(() => Promise.resolve(defaultResponse)),
      post: jest.fn(() => Promise.resolve(defaultResponse)),
      delete: jest.fn(() => Promise.resolve(defaultResponse)),
      defaults: { headers: { common: {} } },
    });
  },
  instance: {},
};

__mock.reset();

module.exports = {
  __mock,
  create() {
    return __mock.instance;
  },
};

My test looks like so:

import React from 'react';
import renderer from 'react-test-renderer';
import { render, fireEvent, screen } from '@testing-library/react';
import mockAxios from 'axios';
import Profile from '../Profile/Profile';

describe('Profile', () => {
  beforeEach(() => {
    mockAxios.__mock.reset();
  });

  it('renders correctly', () => {
    const tree = renderer.create(<Profile />).toJSON();
    expect(tree).toMatchSnapshot();
  });

  it('submits form successfully', () => {
    const { findByLabelText } = render(<Profile />);
    const firstName = findByLabelText('first name');
    firstName.value = 'Michaux';
    const lastName = findByLabelText('last name');
    lastName.value = 'Kelley';
    const email = findByLabelText('email');
    email.value = '[email protected]';
    const submit = screen.getByText('Submit');
    const { put } = mockAxios.__mock.instance;
    put.mockImplementationOnce(() =>
      Promise.resolve({
        data: {},
      })
    );
    fireEvent.click(submit);
    expect(mockAxios.__mock.instance.put).toHaveBeenCalledTimes(1);
    expect(mockAxios.__mock.instance.put).toHaveBeenCalledWith('/');
  });
});

My form is built using formik:

import React, { Component } from 'react';
import { withFormik } from 'formik';
import { toast } from 'react-toastify';
import { Field } from 'formik';
import * as Yup from 'yup';

import api from '../../api';
import TextInput from '@components/common/forms/TextInput';
import Checkbox from '@components/common/forms/Checkbox';
import CheckboxGroup from '@components/common/forms/CheckboxGroup';
import styles from './profile.module.css';
import commonStyles from '@components/common/common.module.css';
import ForgotPassword from '../ForgotPassword';
import Modal from '@components/Modal/Modal';

const formikEnhancer = withFormik({
  validationSchema: Yup.object().shape({
    firstName: Yup.string()
      .min(2, "C'mon, your first name is longer than that")
      .required('First name is required'),
    lastName: Yup.string()
      .min(2, "C'mon, your last name is longer than that")
      .required('Last name is required'),
    email: Yup.string()
      .email('Invalid email address')
      .required('Email is required'),
    currentPassword: Yup.string(),
    password: Yup.string()
      .min(8, 'Password has to be at least 8 characters!')
      .matches(
        /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\\$%\\^&\\*])/,
        'Password must contain at least 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character'
      ),
    confirmPassword: Yup.string().oneOf([Yup.ref('password'), null], 'Passwords must match'),
  }),
  handleSubmit: (payload, { setSubmitting, setErrors, props }) => {
    // TODO: consider putting user in local storage
    const { user } = props;
    api
      .put(`/users/${user.userId}`, payload, {
        withCredentials: true,
      })
      .then(res => {
        toast.success('Your user profile was updated successfully', {
          position: toast.POSITION.TOP_CENTER,
          hideProgressBar: true,
        });
      })
      .catch(err => {
        toast.error('Something went wrong', {
          position: toast.POSITION.TOP_CENTER,
          hideProgressBar: true,
        });
      });
    setSubmitting(false);
  },
  mapPropsToValues: ({ user }) => ({
    ...user,
  }),
  displayName: 'ProfileForm',
});

class Profile extends Component {
  modalProps = {
    triggerText: 'Forgot Password?',
  };

  modalContent = <ForgotPassword user={{ email: '' }} {...this.props} />;

  render() {
    document.title = 'User Profile';
    const {
      values,
      touched,
      errors,
      handleChange,
      handleBlur,
      handleSubmit,
      isSubmitting,
      isAdmin,
      setFieldValue,
    } = this.props;

    return (
      <React.Fragment>
        <h2>Profile</h2>
        <form onSubmit={handleSubmit}>
          <TextInput
            id="firstName"
            type="text"
            label="First Name"
            error={touched.firstName && errors.firstName}
            value={values.firstName}
            onChange={handleChange}
            onBlur={handleBlur}
          />
          <TextInput
            id="lastName"
            type="text"
            label="Last Name"
            error={touched.lastName && errors.lastName}
            value={values.lastName}
            onChange={handleChange}
            onBlur={handleBlur}
          />
          <TextInput
            id="email"
            type="email"
            label="Email"
            autoComplete="username email"
            error={touched.email && errors.email}
            value={values.email}
            onChange={handleChange}
            onBlur={handleBlur}
          />
          <TextInput
            id="currentPassword"
            type="password"
            label="Current Password"
            autoComplete="current-password"
            error={touched.currentPassword && errors.currentPassword}
            value={values.currentPassword}
            onChange={handleChange}
            onBlur={handleBlur}
          />
          <div className={styles.forgotPassword}>
            <Modal
              modalProps={this.modalProps}
              modalContent={this.modalContent}
              modalButtonClassName={commonStyles.modalLinkButton}
              {...this.props}
            />
          </div>
          <TextInput
            id="password"
            type="password"
            label="New Password"
            autoComplete="new-password"
            error={touched.password && errors.password}
            value={values.password}
            onChange={handleChange}
            onBlur={handleBlur}
          />
          <TextInput
            id="confirmPassword"
            type="password"
            label="Confirm Password"
            autoComplete="new-password"
            error={touched.confirmPassword && errors.confirmPassword}
            value={values.confirmPassword}
            onChange={handleChange}
            onBlur={handleBlur}
          />
          {isAdmin && (
            <React.Fragment>
              <h3>Roles</h3>
              <CheckboxGroup
                id="roles"
                className={styles.roles}
                label="Which of these?"
                value={values.roles}
                onChange={setFieldValue}
              >
                <Field
                  component={Checkbox}
                  name="roles"
                  id="admin"
                  label="Admin"
                  value="admin"
                  defaultChecked={values.roles.includes('admin')}
                />
                <Field
                  component={Checkbox}
                  name="roles"
                  id="user"
                  label="User"
                  value="user"
                  defaultChecked={values.roles.includes('user')}
                />
                <Field
                  component={Checkbox}
                  name="roles"
                  id="family"
                  label="Family"
                  value="family"
                  defaultChecked={values.roles.includes('family')}
                />
                <Field
                  component={Checkbox}
                  name="roles"
                  id="friend"
                  label="Friend"
                  value="friend"
                  defaultChecked={values.roles.includes('friend')}
                />
              </CheckboxGroup>
            </React.Fragment>
          )}
          <button type="submit" disabled={isSubmitting} className="btn btn-primary">
            Submit
          </button>
        </form>
      </React.Fragment>
    );
  }
}

export default formikEnhancer(Profile);

How do I get the test to tell me if my axios put mock was called?

like image 220
mkelley33 Avatar asked Dec 22 '25 02:12

mkelley33


1 Answers

UPDATE

I can now see three issues to your implementation.

  1. You create a new Axios instance by using axios.create. You'll have also to mock axios.create with itself:
axios.create = jest.fn(() => axios);
  1. The mock implementation is slightly wrong. Using latest Jest and according to the documentation for Manual Mocks, the implementation of the __mocks__/axios.js should be like this:
const axios = jest.genMockFromModule('axios');
const defaultResponse = { data: {} };

axios.doMockReset = () => {
  Object.assign(axios, {
    get: jest.fn().mockImplementationOnce(() => Promise.resolve(defaultResponse)),
    put: jest.fn().mockImplementationOnce(() => Promise.resolve(defaultResponse)),
    post: jest.fn().mockImplementationOnce(() => Promise.resolve(defaultResponse)),
    delete: jest.fn().mockImplementationOnce(() => Promise.resolve(defaultResponse)),
    defaults: { headers: { common: {} } },
  });
}

// Add create here.
// There is no need to call doMockReset, it will be called when test starts
axios.create = jest.fn(() => axios);

module.exports = axios;
  1. Formik calls handleSubmit asynchronously and Jest does not wait for it to finish. We can use @testing-library/react waitFor to wait for the expect(mockAxios.put).toHaveBeenCalledTimes(1). Also, have the inputs update the values using the fireEvent.change event instead of directly setting the input value:
import { render, fireEvent, waitFor } from '@testing-library/react';
...

await waitFor(() => {
  fireEvent.change(firstName, {
    target: {
      value: "Michaux"
    }
  });
});

...

await waitFor(() => {
  expect(mockAxios.put).toHaveBeenCalledTimes(1);
});

The whole test:

import React from 'react';
import renderer from 'react-test-renderer';
import { render, fireEvent, waitFor } from '@testing-library/react';
import mockAxios from 'axios';
import Profile from '../Profile/Profile';

describe('Profile', () => {
  beforeEach(() => {
    mockAxios.doMockReset();
  });

  it('renders correctly', () => {
    const tree = renderer.create(<Profile />).toJSON();
    expect(tree).toMatchSnapshot();
  });

  it('submits form successfully', async () => {
    // Pass a fake user with formik default values for use in mapPropsToValues,
    // else React will complaint regarding inputs changed
    // from uncontrolled to controlled because of these values initially set to undefined:
    // Warning: A component is changing an uncontrolled input of type text to be controlled

    const user = { userId: 1, firstName: '', lastName: '', email: '' };

    const { getByText, getByLabelText } = render(<Profile user={user} />);

    const firstName = getByLabelText(/first name/i);
    const lastName = getByLabelText(/last name/i);
    const email = getByLabelText(/email/i);
    const submit = getByText('Submit');

    await waitFor(() => {
      fireEvent.change(firstName, {
        target: {
          value: "Michaux"
        }
      });
    });

    await waitFor(() => {
      fireEvent.change(lastName, {
        target: {
          value: "Kelley"
        }
      });
    });

    await waitFor(() => {
      fireEvent.change(email, {
        target: {
          value: "[email protected]"
        }
      });
    });

    fireEvent.click(submit);

    await waitFor(() => {
      expect(mockAxios.put).toHaveBeenCalledTimes(1);
    })
  });
});

like image 109
Christos Lytras Avatar answered Dec 26 '25 02:12

Christos Lytras



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!