【问题标题】:How to mock data from service as hook with React Testing Library?如何使用 React 测试库将服务中的数据模拟为钩子?
【发布时间】:2025-12-19 17:25:12
【问题描述】:

我开始使用 React 测试库 为 React 应用程序进行测试,但我正在努力模拟组件中的数据,该组件使用 Hook 进行 API 调用 strong> 作为服务。

我的组件只是一个功能组件,没什么特别的:

import React, { useEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import Skeleton from '@material-ui/lab/Skeleton'

import usePanelClient from '../../../clients/PanelClient/usePanelClient'
import useUtils from '../../../hooks/useUtils'
import IconFile from '../../../assets/img/panel/ico-file.svg'
import IconExclamation from '../../../assets/img/panel/ico-exclamation.svg'
import IconPause from '../../../assets/img/panel/awesome-pause-circle.svg'
import './PendingAwards.scss'

const PendingAwards = () => {
  const pendingAwardsRef = useRef(null)
  const location = useLocation()

  const [loading, setLoading] = useState(true)
  const [pendingAwards, setPendingAwards] = useState({})
  const panelClient = usePanelClient()
  const { formatCurrency } = useUtils()

  useEffect(() => {
    panelClient()
      .getPendingAwards()
      .then((response) => {
        setLoading(false)
        setPendingAwards(response.data)
      })
  }, [panelClient])

  useEffect(() => {
    const searchParams = new URLSearchParams(location.search)
    if (searchParams.get('scrollTo') === 'pendingAwards') {
      window.scrollTo({
        top: 0,
        behavior: 'smooth',
      })
    }
  }, [location])

  return (
    <div
      id="pendingAwards"
      className="pending-awards-container"
      ref={pendingAwardsRef}
    >
      <span className="pending-awards-container__title">Prêmios Pendentes</span>
      {loading && (
        <div className="skeleton-box">
          <Skeleton width="100%" height="70px" />
          <Skeleton width="100%" height="70px" />
        </div>
      )}

      {!loading && (
        <div className="pending-awards-values">
          <div className="pending-awards-container__quantity">
            <div className="pending-awards-container__quantity-container">
              <span className="pending-awards-container__quantity-container-title">
                Quantidade
              </span>
              <span className="pending-awards-container__quantity-container-content">
                <div className="pending-awards-container__quantity-container-content__icon-box">
                  <img src={IconFile} alt="Ícone de arquivo" />
                </div>
                {pendingAwards.quantity ? pendingAwards.quantity : '0'}
              </span>
            </div>
          </div>

          <div className="pending-awards-container__amount">
            <div className="pending-awards-container__amount-container">
              <span className="pending-awards-container__amount-container-title">
                Valor Pendente
              </span>
              <span className="pending-awards-container__amount-container-content">
                <div className="pending-awards-container__amount-container-content__icon-box">
                  <img src={IconPause} alt="Ícone Pause" />
                </div>
                {pendingAwards.amount
                  ? formatCurrency(pendingAwards.amount)
                  : 'R$ 0,00'}
              </span>
            </div>
          </div>
          <div className="pending-awards-container__commission">
            <div className="pending-awards-container__commission-container">
              <span className="pending-awards-container__commission-container-title">
                Comissão Pendente
              </span>
              <span className="pending-awards-container__commission-container-content">
                <div className="pending-awards-container__commission-container-content__icon-box">
                  <img src={IconExclamation} alt="Ícone exclamação" />
                </div>
                {pendingAwards.commission
                  ? formatCurrency(pendingAwards.commission)
                  : 'R$ 0,00'}
              </span>
            </div>
          </div>
        </div>
      )}
    </div>
  )
}
export default PendingAwards

我的调用 API 的服务是这样写的:

import { useCallback } from 'react'
import axios from 'axios'

const usePanelClient = () => {
  const getQuotationCard = useCallback(() => axios.get('/api/cards/quotation'), [])
  const getCommissionCard = useCallback(() => axios.get('/api/cards/commission'), [])
  const getPendingAwards = useCallback(() => axios.get('/api/premium/pending'), [])

  return useCallback(() => ({
    getQuotationCard,
    getCommissionCard,
    getPendingAwards,
  }), [
    getQuotationCard,
    getCommissionCard,
    getPendingAwards,
  ])
}

export default usePanelClient

在我当前的测试中,我尝试过像这样模拟钩子,但我没有成功:

import React from 'react'
import { render } from '@testing-library/react'
import { Router } from 'react-router-dom'
import { createMemoryHistory } from 'history'

import PendingAwards from './PendingAwards'

describe('PendingAwards Component', () => {
  beforeEach(() => {
    jest.mock('../../../clients/PanelClient/usePanelClient', () => {
      const mockData = {
        quantity: 820,
        amount: 26681086.12,
        commission: 5528957.841628,
      }
      return {
        getPendingAwards: jest.fn(() => Promise.resolve(mockData)),
      }
    })
  })

  it('should render the PendingAwards', () => {
    const history = createMemoryHistory()
    history.push = jest.fn()
    const { container } = render(
      <Router history={history}>
        <PendingAwards />
      </Router>,
    )
    expect(container).toBeInTheDocument()
  })

  it('should render the PendingAwards', () => {
    const history = createMemoryHistory()
    history.push({
      search: '&scrollTo=pendingAwards',
    })
    window.scrollTo = jest.fn()
    render(
      <Router history={history}>
        <PendingAwards />
      </Router>,
    )
    expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 })
  })
})

有人可以帮我解决这个问题吗?我觉得这不是什么难事,但我尝试了几件事,似乎没有任何解决办法。

提前致谢。

【问题讨论】:

  • 您应该将 jest.mock 移到 beforeEach 块之外。我看到的另一个问题是模拟数据应该有一个名为 data 的属性,因为它是您在组件中所期望的 (response.data)
  • @lissettdm 我会试着改成那个,你
  • 我试过这样做,但似乎没有用:jest.mock('../../../clients/PanelClient/usePanelClient', () =&gt; { const mockData = { quantity: 820, amount: 26681086.12, commission: 5528957.841628, } return { getPendingAwards: jest.fn(() =&gt; Promise.resolve({ data: mockData })), } }) 我也收到此错误:UnhandledPromiseRejectionWarning: E​​rror: Network Error

标签: reactjs jestjs react-testing-library


【解决方案1】:

您必须在模块的顶层调用jest.mock,并且要使用默认导出模拟ES6模块,您应该使用__esModule: true

jest.mock('../../../clients/PanelClient/usePanelClient', () => {
  const mockData = {
    quantity: 820,
    amount: 26681086.12,
    commission: 5528957.841628,
  }
  return {
    __esModule: true,
    default: ()=> ({
      getPendingAwards: jest.fn(() => Promise.resolve({data: mockData})),
    }),
}});

【讨论】:

  • 我收到此警告,并且组件中似乎未返回模拟结果:(node:91604) UnhandledPromiseRejectionWarning: E​​rror: Network Error
  • 如果您遇到网络错误是因为模拟不起作用并且已调用 axios 函数。在 jest docs 中有一个警告:在安装文件中导入模块(由 setupFilesAfterEnv 指定)将阻止对有问题的模块以及它导入的所有模块进行模拟。见jestjs.io/docs/jest-object#jestmockmodulename-factory-options
  • 我的安装文件只有这个:import '@testing-library/jest-dom/extend-expect'
  • 根据您的测试文件,您确定此路径“../../../clients/PanelClient/usePanelClient”是否正常?
  • 我想是的,因为我也在我的组件上使用了这个钩子。我必须将 jest.mock... 放在 describe 中,对吗?