Как смоделировать топор ios для отправки формы? - PullRequest
3 голосов
/ 08 мая 2020

Проблема, с которой я столкнулся, заключается в том, что я не могу проверить, был ли вызван метод ax ios put после нажатия кнопки отправки.

У меня в __mocks __ / ax есть следующее ios. 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;
  },
};

Мой тест выглядит так:

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 = 'test@test.com';
    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('/');
  });
});

Моя форма построена с использованием 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);

Как мне получить тест, чтобы узнать, был ли вызван мой топор ios put mock?

1 Ответ

1 голос
/ 09 июня 2020

ОБНОВЛЕНИЕ

Теперь я вижу три проблемы в вашей реализации.

  1. Вы создаете новый экземпляр Ax ios, используя axios.create. Вам также придется издеваться над axios.create с собой:
axios.create = jest.fn(() => axios);
Мок-реализация немного неверна. Используя последнюю версию Jest и согласно документации для Manual Mocks , реализация __mocks__/axios.js должна быть такой:
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;
Formik вызывает handleSubmit асинхронно, а Jest не ждет, пока он закончит sh. Мы можем использовать @testing-library/react waitFor, чтобы дождаться expect(mockAxios.put).toHaveBeenCalledTimes(1). Кроме того, пусть входные данные обновят значения с помощью события fireEvent.change вместо непосредственной установки входного значения:
import { render, fireEvent, waitFor } from '@testing-library/react';
...

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

...

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

Весь тест:

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: "test@test.com"
        }
      });
    });

    fireEvent.click(submit);

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

...