如何为 formik 提交模拟 axios?
How to mock axios for formik submit?
我 运行 遇到的问题是无法测试在单击提交按钮后是否调用了 axios put 方法。
我的 __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;
},
};
我的测试是这样的:
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);
如何让测试告诉我是否调用了 axios put mock?
更新
我现在可以看出您的实施存在三个问题。
- 您使用
axios.create
创建了一个新的 Axios 实例。您还必须自己模拟 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 不等待它完成。我们可以用@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);
})
});
});
我 运行 遇到的问题是无法测试在单击提交按钮后是否调用了 axios put 方法。
我的 __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;
},
};
我的测试是这样的:
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);
如何让测试告诉我是否调用了 axios put mock?
更新
我现在可以看出您的实施存在三个问题。
- 您使用
axios.create
创建了一个新的 Axios 实例。您还必须自己模拟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 不等待它完成。我们可以用@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);
})
});
});