React项目MSW+Axios+Jest测试:POST请求Mock未触发求助
问题描述
尝试Mock返回404状态的API POST请求,期望请求响应后显示带有data-testid="error-pw-api"的提示弹窗,但MSW的POST Mock未触发,测试提示找不到该元素。
使用的依赖版本:
axios: ^0.27.2msw: ^0.44.2
组件代码
import React, { useState } from 'react' import { Alert, Modal, Spinner } from 'react-bootstrap' import { toaster, translation } from '../../models/main'; import { useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import { passwordSchema } from '../../utils/validationSchemas'; import axios from 'axios'; type Props = { isModalOpen: boolean; closeModal: () => void; l: translation; toaster: toaster; userName: string } const PasswordChange = ({ isModalOpen, closeModal, l, userName, toaster }: Props) => { const { register, handleSubmit, formState: { errors }, reset, clearErrors } = useForm({ mode: "onSubmit", resolver: yupResolver(passwordSchema(userName, l)), }); const [isPasswordVisible, setIsPasswordVisible] = useState(false) const [isLoading, setIsLoading] = useState(false) const [serverError, setServerError] = useState("") const submitChanges = async (data) => { clearErrors() setIsLoading(true); try { const url = '/user/' + userName + '/password' const result = await axios.post(url, { password: data.passwordOne, oldPassword: data.oldPassword }) toaster.success(l("account_settings.message.password_changed_success")) closeModal() setServerError("") reset() } catch (err) { const errorMessage = err.response.status === 404 && err.response.data.message ? l("account_settings.message.old_password_error") : err.response.statusText; setServerError(l("account_settings.message.server_error") + errorMessage) } finally { setIsLoading(false) } } const handleClose = () => { reset() closeModal() setServerError("") setIsLoading(false) setIsPasswordVisible(false) } return ( <Modal size="lg" show={isModalOpen} onHide={handleClose} aria-labelledby="example-modal-sizes-title-lg" > <Modal.Header closeButton> <Modal.Title id="example-modal-sizes-title-lg"> {l("account_settings.change_password")} </Modal.Title> </Modal.Header> <form onSubmit={handleSubmit(data => submitChanges(data))}> <Modal.Body> <div className="form-group"> <label htmlFor="oldPassword">{l("account_settings.enter_old_password_label")}</label> <input required id="oldPassword" name="oldPassword" data-testid="oldPassword" type={isPasswordVisible ? "text" : "password"} className={`form-control ${errors.oldPassword ? "is-invalid" : "border border-dark"}`} {...register("oldPassword")} placeholder={l("account_settings.old_password_placeholder")} /> {errors.oldPassword && <div className="invalid-feedback" data-testid="error-oldpw"> {errors.oldPassword.message} </div>} </div> <div className="form-group"> <label htmlFor="passwordOne">{l("account_settings.enter_password")}</label> <input required id="passwordOne" data-testid="passwordOne" name="passwordOne" type={isPasswordVisible ? "text" : "password"} className={`form-control ${errors.passwordOne ? "is-invalid" : "border border-dark"}`} {...register("passwordOne")} placeholder={l("account_settings.password")} /> {errors.passwordOne && <div className="invalid-feedback" data-testid="error-pw-one"> {errors.passwordOne.message} </div>} </div> <div className="form-group"> <label htmlFor="passwordTwo">{l("account_settings.enter_password_again")}</label> <input required id="passwordTwo" data-testid="passwordTwo" name="passwordTwo" type={isPasswordVisible ? "text" : "password"} className={`form-control ${errors.passwordTwo ? "is-invalid" : "border border-dark"}`} {...register("passwordTwo")} placeholder={l("account_settings.verify_password")} /> {errors.passwordTwo && <div className="invalid-feedback" data-testid="error-pw-two"> {errors.passwordTwo.message} </div>} </div> {serverError && <div className="my-2" datat-testid="error-pw-api"> <Alert variant="danger"> <i className="fa-solid fa-triangle-exclamation"></i> {serverError} </Alert> </div>} </Modal.Body> <Modal.Footer> <button className="btn btn-outline-dark" type="button" onClick={() => setIsPasswordVisible(prev => !prev)} > {isPasswordVisible ? <i className="fas fa-eye mt-2" /> : <i className="fas fa-eye-slash mt-2"></i> } </button> <button id="submit-pw-change" data-testid="submit-pw-change" disabled={isLoading} type="submit" className="btn btn-primary"> {isLoading && <Spinner data-testid="spinner-btn-pw-change" animation="border" variant="light" size='sm' className="mx-1" />} {l("account_settings.change_password_button")} </button> <button type="button" className="btn btn-outline-dark" onClick={handleClose}> {l("buttons.cancel")} </button> </Modal.Footer> </form> </Modal> ) } export default PasswordChange;
测试文件代码
import { fireEvent, render, cleanup } from "@testing-library/react"; import React from "react"; import { act } from "react-dom/test-utils"; import PasswordChange from "../../src/components/UserDetails/passwordChange"; import { mockedToaster, mockFunc } from "./userDetails.test"; import { server } from "../__mocks__/serverMock"; import { rest } from "msw"; jest.mock("axios") beforeAll(() => server.listen()); afterAll(() => server.close()); afterEach(() => server.resetHandlers()); describe("Server error tests", () => { it("Test validation with ser verror", async () => { await act(async () => { const { findByTestId, getByTestId, queryByTestId } = render(<PasswordChange isModalOpen={true} closeModal={mockFunc} l={mockedL} toaster={mockedToaster} userName="randomname" />) fireEvent.change(await getByTestId("oldPassword"), { target: { value: 'RandomGang420££' } }) fireEvent.change(await getByTestId("passwordOne"), { target: { value: 'RandomGang420££' } }) fireEvent.change(await getByTestId("passwordTwo"), { target: { value: 'RandomGang420££' } }) fireEvent.submit(await getByTestId("submit-pw-change")); server.use(rest.post("/user/:user/password", async (req, res, ctx) => { return res( ctx.status(404), ) })) expect(await queryByTestId("error-pw-one")).toBeNull() expect(await queryByTestId("error-pw-two")).toBeNull() expect(await findByTestId("error-pw-api")).toBeInTheDocument() }) }); })
解决方案
你的代码存在三个关键问题,逐一修复即可解决:
1. 移除jest.mock("axios"),让MSW能拦截真实请求
jest.mock("axios")会将axios替换为Jest的模拟函数,MSW无法拦截这种模拟请求。删除这行代码,让组件使用真实的axios实例,MSW才能正常拦截并返回Mock响应。
2. 提前注册MSW的请求处理器
当前代码是在提交表单后才添加MSW的handler,此时请求已经发出,handler来不及生效。需要在渲染组件之前就注册好Mock处理器:
it("Test validation with server error", async () => { // 提前注册Mock处理器 server.use(rest.post("/user/:user/password", async (req, res, ctx) => { return res( ctx.status(404), ctx.json({ message: "Old password incorrect" }) // 可选:添加响应数据,匹配组件逻辑 ) })) const { findByTestId, getByTestId, queryByTestId } = render(<PasswordChange isModalOpen={true} closeModal={mockFunc} l={mockedL} toaster={mockedToaster} userName="randomname" />) fireEvent.change(getByTestId("oldPassword"), { target: { value: 'RandomGang420££' } }) fireEvent.change(getByTestId("passwordOne"), { target: { value: 'RandomGang420££' } }) fireEvent.change(getByTestId("passwordTwo"), { target: { value: 'RandomGang420££' } }) fireEvent.submit(getByTestId("submit-pw-change")); expect(await queryByTestId("error-pw-one")).toBeNull() expect(await queryByTestId("error-pw-two")).toBeNull() expect(await findByTestId("error-pw-api")).toBeInTheDocument() })
3. 修复组件中data-testid的拼写错误
组件里的错误提示容器写的是datat-testid="error-pw-api",少了一个字母a,正确应该是data-testid="error-pw-api",否则测试无法定位到该元素:
{serverError && <div className="my-2" data-testid="error-pw-api"> <Alert variant="danger"> <i className="fa-solid fa-triangle-exclamation"></i> {serverError} </Alert> </div>}
额外优化:移除不必要的act包裹
React Testing Library的异步查询方法(如findByTestId)已经内置了act的处理逻辑,不需要手动用act包裹整个测试代码,简化测试结构。
内容的提问来源于stack exchange,提问作者eko
相关产品推荐
相关产品推荐

