在 React 测试库中运行异步测试时,Apollo useQuery() 抛出错误:ECONNREFUSED

问题描述 投票:0回答:2

更新: 尝试升级到 React 18 和 RTL 13,但这并没有解决问题。

  • 反应16.4.0
  • 反应脚本5.0.1
  • 阿波罗客户端3.5.9
  • React 测试库 12.1.2

我在测试 React 组件时遇到了长期问题,这些组件在安装时会调用 Apollo

useQuery()
。如果我使用异步 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
是未定义的。我无法测试网格是否渲染了表格,因为在这种情况下,组件只是返回
<div>loading...</div>
。我尝试将渲染函数包装在
act()
中,但这不会改变任何东西。我也试过
waitFor()
没有成功。

我还尝试了同步代码并将

getBy()
调用和断言包装在
setTimeout()
中。有趣的是,这实际上会产生误报。网格不会渲染,我可以断言任何我想要的东西并且它会通过。 (不确定这是 Jest bug 还是 RTL bug),

我将包含测试的完整代码。请注意,我正在将一些数据写入客户端缓存。这是因为除了发出网络请求之外,组件还通过使用

@client
指令查询属性来获取一些本地状态。

以下是疑问:

import gql from 'graphql-tag'

// this data is in the client-side cache only
// it is local state
// we write this to the cache in the test
export const GET_REPORTING_GRID_SETTINGS = gql`
    query GetReportingGridSettings {
        reportingGridQueryVariables @client
    }
`
// this data is in the client-side cache only
// it is local state
// we write this to the cache in the test
export const GET_COLUMN_SELECTIONS = gql`
    query ColumnSelections {
        columnSelections @client {
            household
            individual
            cases
            activities
        }
    }
`
// this data is in the client-side cache only
// it is local state
// we write this to the cache in the test
export const GET_REPORTING_MODAL_STATE = gql`
    query GetReportingModalState {
        showReportingMainModal @client
    }
`

// this is a network query
// we pass this in 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
        }
    }
`

// this is a network query
// we pass this in 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'

// Set dateRanges in the cache to match the values used in the Apollo mocks
beforeEach(() => {
  // Set dateRanges in the cache to match the values used in the Apollo mocks
  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'

/**
 * [dispatch Dispatch from our store
 * @type {Function}
 */
const { dispatch } = store

/**
 * Generates a mock store given an initial state. Middlewares are optional and
 * defaults to thunk. The mock store is used to set initial application state to
 * supply data to UI components. The mock store can also be used to test for
 * dispatched actions.
 *
 * Reducers can be unit tested separately. Testing the full range of user
 * interaction, to action dispatch, to reducer input/output, to state change,
 * would be done in an integration test. The mock store does not invoke reducers
 * or change state.
 *
 * {@link https://github.com/reduxjs/redux-mock-store|redux-mock-store}
 *
 * @param  {Object} initialState Initial redux state
 * @param  {Array}  middlewares  Optional middleware, default: [thunk]
 * @return {Object}              Mock 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>
    )

// Did we receive a (mocked) reduxStore property in the optional second argument?
// If so, pass it in to WrapperWithProviders
// If not, pass the default redux store imported at the top of this file
const getReduxStore = (options) =>
  options && options.reduxStore ? options.reduxStore : store

// Did we receive an optional second argument?
// Did it contain a reduxStore property?
// If so, remove it from the options before passing them to RTL render()
// (That argument is for WrapperWithProviders, not 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))
}
// re-export everything
export * from '@testing-library/react'

// override render method
export {
  customRender as render,
  createMockStore,
  customRenderHook as renderHook,
  dispatch,
}
javascript reactjs create-react-app apollo-client react-testing-library
2个回答
0
投票

恐怕我对 Apollo 库不熟悉,但在我看来,模拟不起作用;相反,它正在尝试对 localhost:80 进行真正的 http 调用。

我希望当您不“等待”时看不到错误的原因是因为http请求本身是异步的,因此如果没有“等待”,它在执行断言之前没有机会运行在测试中。

查看代码,我猜测您正在用数据预填充缓存,因此它不应该尝试网络访问?我建议使用调试器单步执行“mount”函数或挂钩,并尝试查看模拟数据是否最终达到您期望的位置。我经常对 java 脚本值没有达到我预期的结果感到困惑!

抱歉我无法提供更多帮助。


0
投票

可能有点太晚了,但由于我在这个问题上浪费了一些时间,所以值得为未来的读者发布。

您很可能在测试的组件中覆盖了模拟的客户端。

所以你的组件中有这样的东西:

const [getReportedGridSettingsQuery] = useMutation(GET_REPORTING_GRID_SETTINGS, {
    client: createGenericClient()
  })

您必须用

ApolloProvider
包装您的原始组件(或高阶组件),就像您在玩笑测试中所做的那样,这样 Apollo 库就可以用您模拟的提供程序覆盖该提供程序。

<ApolloProvider client={client}>
   <YourComponent/>
</ApolloProvider>
© www.soinside.com 2019 - 2024. All rights reserved.