You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React项目MSW+Axios+Jest测试:POST请求Mock未触发求助

问题描述

尝试Mock返回404状态的API POST请求,期望请求响应后显示带有data-testid="error-pw-api"的提示弹窗,但MSW的POST Mock未触发,测试提示找不到该元素。

使用的依赖版本:

  • axios: ^0.27.2
  • msw: ^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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 12:33:17