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
相关产品推荐
相关产品推荐

