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

RTL异步测试Apollo useQuery组件抛出ECONNREFUSED错误

问题描述

更新说明: 曾尝试升级至React 18与React Testing Library 13版本,但该问题并未得到修复。

运行环境版本

  • React 16.4.0
  • react-scripts 5.0.1
  • Apollo Client 3.5.9
  • React Testing Library 12.1.2

测试挂载阶段调用Apollo useQuery() 钩子的React组件时存在持续性问题:

  • 若使用RTL的异步查询方法,GraphQL请求会抛出 ECONNREFUSED 连接拒绝错误
  • 若使用RTL的同步查询方法则不会触发该错误,但同步方法无法满足测试需求:断言执行时组件的loading状态仍为true,查询数据尚未返回,组件无实际业务内容可供渲染验证。

触发错误的异步测试代码

it('renders', async () => {
  renderGrid()
  const columnHeading = await screen.findByText(/Primary Household Member/i)
  expect(columnHeading).toBeInTheDocument()
})

控制台对应报错输出见下方截图。

如果改为同步写法:

it('renders', () => {
  renderGrid()
  const columnHeading = screen.getByText(/Primary Household Member/i)
  expect(columnHeading).toBeInTheDocument()
})

此时不会出现ECONNREFUSED错误,但组件的loading状态为true,data与error字段均为undefined,组件仅返回<div>loading...</div>加载提示,无法验证网格表格的渲染逻辑。
此前已尝试使用act()包裹render函数、调用waitFor()方法等待渲染更新,均未解决问题。
尝试在同步代码中使用setTimeout()包裹getBy()调用与断言逻辑时,会出现假阳性结果:即使网格未正常渲染,任意断言都会通过(暂不确定该现象是Jest还是RTL的bug)。


前置说明

测试前已预先向客户端缓存写入了部分数据,组件除发起网络请求外,还会通过带@client指令的查询读取部分本地状态。

测试涉及的GraphQL查询

import gql from 'graphql-tag'

// 仅存在于客户端缓存的本地状态数据,测试时提前写入缓存
export const GET_REPORTING_GRID_SETTINGS = gql`
    query GetReportingGridSettings {
        reportingGridQueryVariables @client
    }
`
// 仅存在于客户端缓存的本地状态数据,测试时提前写入缓存
export const GET_COLUMN_SELECTIONS = gql`
    query ColumnSelections {
        columnSelections @client {
            household
            individual
            cases
            activities
        }
    }
`
// 仅存在于客户端缓存的本地状态数据,测试时提前写入缓存
export const GET_REPORTING_MODAL_STATE = gql`
    query GetReportingModalState {
        showReportingMainModal @client
    }
`

// 网络请求查询,通过apollo mocks传入模拟数据
export const ME = gql`
    query me {
        me {
            id
            isACaseManager
            fullName
            role
            userable {
                ... on CaseManager {
                    id
                    organization {
                        id
                        name
                        slug
                    }
                    locations {
                        id
                        name
                        slug
                        customFields {
                            id
                            label
                        }
                    }
                }
            }
            email
        }
    }
`

// 网络请求查询,通过apollo mocks传入模拟数据
export const GET_INDIVIDUAL_DEMOGRAPHICS = gql`
    query getDemographics(
        $pageSize: Int
        $pageNumber: Int
        $sort: [IndividualDemographicReportSortInput!]
        $filter: IndividualDemographicReportFilterInput
        $searchTerm: String
    ) {
        individualDemographicReport(
            pageSize: $pageSize
            pageNumber: $pageNumber
            sort: $sort
            filter: $filter
            searchTerm: $searchTerm
        ) {
            totalCount
            pageCount
            nodes {
                id
                fullName
                annualIncome
                age
                dateOfBirth
                displayDateOfBirth @client #type policy
                relationshipToClient
                employmentCount
                isCurrentlyWorking
                displayIsCurrentlyWorking @client #type policy
                employmentStatus
                additionalIncome
                displayAdditionalIncome @client #type policy
                alimonyAmount
                primaryAccountHolder {
                    fullName
                    clientLocations {
                        id
                    }
                    lastYearAdjustedGrossIncome
                    taxFilingStatus
                    displayLastYearAdjustedGrossIncome @client #type policy
                    displayTaxFilingStatus @client #type policy
                }
                demographic {
                    id
                    gender
                    race
                    ethnicity
                    education
                    healthInsurance
                    hasHealthInsurance
                    displayHasHealthInsurance @client #type policy
                    isStudent
                    displayIsStudent @client #type policy
                    isVeteran
                    displayIsVeteran @client #type policy
                    isDisabled
                    isPregnant
                    displayIsPregnant @client #type policy
                    isUsCitizen
                    displayIsUsCitizen @client #type policy
                    immigrationStatus
                    lengthOfPermanentResidency
                    courseLoad
                    hasWorkStudy
                    displayHasWorkStudy @client #type policy
                    expectedFamilyContribution
                    costOfAttendance
                    displayEfc @client #type policy
                    displayCoa @client #type policy
                    courseLoad
                }
                alimonyAmount
                childSupportAmount
                pensionAmount
                ssdSsiAmount
                unemploymentInsuranceAmount
                vaBenefitsAmount
                workersCompensationAmount
                otherAdditionalIncomeAmount
                savingsAmount
                claimedAsDependent
                displayClaimedAsDependent @client #type policy
            }
        }
    }
`

测试文件完整代码

import React from 'react'
import { render, screen } from 'Utils/test-utils'
import {
  GET_INDIVIDUAL_DEMOGRAPHICS,
  GET_REPORTING_GRID_SETTINGS,
  GET_COLUMN_SELECTIONS,
  GET_REPORTING_MODAL_STATE,
} from 'Components/Reporting/Hooks/gql'
import mockCache, {
  reportingIndividualDateRangeStartVar,
  reportingIndividualDateRangeEndVar,
} from 'ApolloClient/caseManagementCache'
import {
  apolloMocks,
  mockReportingGridState,
  mockReportingColumnsData,
  mockReportingModalsData,
} from './fixtures'
import ReportingGrid from './ReportingGrid'
import getColumnsData from 'Components/CaseManagement/Reporting/Grids/Demographics/Individual/columnsData'

// 将缓存中的日期范围设置为与Apollo mock中使用的值一致
beforeEach(() => {
  reportingIndividualDateRangeStartVar('2022-01-01T05:00:00.000Z')
  reportingIndividualDateRangeEndVar('2022-06-03T13:56:36.662Z')

  mockCache.writeQuery(
    {
      query: GET_REPORTING_GRID_SETTINGS,
      data: mockReportingGridState,
    },
    {
      query: GET_COLUMN_SELECTIONS,
      data: mockReportingColumnsData,
    },
    {
      query: GET_REPORTING_MODAL_STATE,
      data: mockReportingModalsData,
    }
  )
})

const renderGrid = () => {
  render(
    <ReportingGrid
      dataQueryTag={GET_INDIVIDUAL_DEMOGRAPHICS}
      defaultSortField={'fullName'}
      getColumnsData={getColumnsData}
      sortable
      pageable
      reportEnum={'INDIVIDUAL'}
    />,
    {
      apolloMocks,
      cache: mockCache,
      addTypename: true,
    }
  )
}

it('renders', () => {
  renderGrid()
  const columnHeading = screen.getByText(/Primary Household Member/i)
  expect(columnHeading).toBeInTheDocument()
})

控制台报错截图


项目test-utils.js文件内容

import React from 'react'
import { Provider as ReduxProvider } from 'react-redux'
import configureStore from 'redux-mock-store'
import { MockedProvider as MockedApolloProvider } from '@apollo/client/testing'
import { render } from '@testing-library/react'
import { renderHook } from '@testing-library/react-hooks'
import { BrowserRouter as Router } from 'react-router-dom'
import store from '../Store'
import thunk from 'redux-thunk'

import { ThemeProvider as StyledThemeProvider } from 'styled-components/macro'
import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles'
import styledTheme from 'Shared/Theme'
import { mainMuiTheme } from 'Shared/Theme/muiTheme'

const { dispatch } = store

/**
 * 基于初始状态生成mock store,中间件可选,默认为thunk。
 * mock store用于设置应用初始状态为UI组件提供数据,也可用于测试派发的action。
 * reducer可单独做单元测试,完整用户交互到action派发、reducer输入输出、状态变更的流程可在集成测试中覆盖。
 * mock store不会触发reducer也不会修改状态。
 *
 * @param  {Object} initialState 初始redux状态
 * @param  {Array}  middlewares  可选中间件,默认值: [thunk]
 * @return {Object}              模拟Redux Store
 */
const createMockStore = (initialState, middlewares = [thunk]) =>
  configureStore(middlewares)(initialState)

const WrapperWithProviders =
  ({ reduxStore, apolloProps }) =>
  ({ children }) =>
    (
      <ReduxProvider store={reduxStore}>
        <MockedApolloProvider {...apolloProps}>
          <StyledThemeProvider theme={styledTheme.mode['light']}>
            <MuiThemeProvider theme={mainMuiTheme}>
              <Router>{children}</Router>
            </MuiThemeProvider>
          </StyledThemeProvider>
        </MockedApolloProvider>
      </ReduxProvider>
    )

// 可选第二个参数中是否传入了mock的reduxStore属性?
// 是则传入WrapperWithProviders,否则使用文件顶部导入的默认redux store
const getReduxStore = (options) =>
  options && options.reduxStore ? options.reduxStore : store

// 是否传入了可选第二个参数?是否包含reduxStore属性?
// 是则在传给RTL render()前从options中移除该属性(该属性仅用于WrapperWithProviders,不属于RTL参数)
const getOptions = (options) => {
  if (!options) return
  const {
    reduxStore,
    apolloMocks,
    addTypename,
    cache,
    resolvers,
    ...remainingOptions
  } = options
  return remainingOptions
}

const mockWrapper = (options) => {
  let apolloMocks = []
  let addTypename = false
  let cache = null
  let resolvers = null

  if (options) {
    apolloMocks = options.apolloMocks || apolloMocks
    addTypename = options.addTypename || addTypename
    cache = options.cache || cache
    resolvers = options.resolvers || resolvers
  }

  const apolloProps = {
    mocks: apolloMocks,
    addTypename,
    resolvers,
  }

  if (cache) {
    apolloProps.cache = cache
  }
  const reduxStore = getReduxStore(options)
  return {
    wrapper: WrapperWithProviders({
      reduxStore,
      apolloProps,
    }),
    ...getOptions(options),
  }
}

const customRender = (ui, options) => {
  return render(ui, mockWrapper(options))
}

const customRenderHook = (cb, options) => {
  return renderHook(cb, mockWrapper(options))
}
// 重新导出所有RTL导出内容
export * from '@testing-library/react'

// 覆盖默认render方法
export {
  customRender as render,
  createMockStore,
  customRenderHook as renderHook,
  dispatch,
}

内容的提问来源于stack exchange,提问作者Neil Girardi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 21:01:02