Apollo useQuery() throws Error: ECONNREFUSED when running async test in React Testing Library

UPDATE: Tried upgrading to React 18 and RTL 13 but that did not fix the issue.

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

I’m having chronic issues with testing React components that call Apollo useQuery() when they mount. If I use async RTL methods, the GraphQL query fails with an ECONNREFUSED error. If I use sync RTL methods I do not get this error. However, sync methods do not work because the assertion runs while loading equals true. Therefore, the data has not come back from the query and thus there is no content to render.

Here is a code snippet from a test that elicits this error:

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

I have attached a screenshot of what this produces in the console (see below). If I change the code to be synchronous like so:

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

I do not get the ECONNREFUSED error. However, in that case, loading is true and data and error are undefined. I cannot test that the grid rendered the table because in this case, the component simply returns <div>loading...</div>. I have tried wrapping the render function in act() but this does not change anything. I have also tried waitFor() to no avail.

I also tried the sync code and wrapping the getBy() call and the assertion in a setTimeout(). Interestingly enough, that actually creates a false positive. The grid doesn't render and I can assert anything I want and it passes. (Not sure if that is a Jest bug or an RTL bug),

I will include the full code for the test. Please note that I am writing some data to the client-side cache. This is because in addition to making the network request, the component fetches some local state by querying properties with the @client directive.

Here is the full content of the test file. It is followed by the console screenshot:

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()
})

enter image description here

Here is out test-utils.js file:

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,
}


Comments

Popular posts from this blog

Spring Elasticsearch Operations

Network Error and Timeout on Authorize.net JS

Object oriented programming concepts (OOPs)