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

2

12

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 are the queries:

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

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,
}
Ceremonial answered 7/6, 2022 at 19:43 Comment(1)
I have not used Apollo or GraphQL extensively, but from the error you get there is definitely a network call being made from the ReportingGrid component and I don't see you mocking any network call. Are you sure the cache is first checked to return the data from the Apollo client and that the mockCache is correctly instantiated? If you manage to get past the error and mock the data I would also suggest to use RTL findByText instead of get since they are async and are better for asserting, you can also pass a bigger timeout if needed as second config object to the findBy* functions.Baggywrinkle
A
0

I'm afraid I am not familiar with the Apollo library but it looks to me like the mock is not working; instead it is attempting a real http call to localhost:80.

I expect that the reason you don't see the error when you don't 'await' is because the http request itself is asynchronous so it does not get a chance to run before you do the assertion if there is no 'await' in the test.

Looking at the code I surmise that you are prepopulating a cache with data so it should not attempt network access? I suggest stepping through the 'mount' function or hook with a debugger and try to see if the mock data is ending up where you expect it to be. I have often got confused with java script values not ending up where I expect!

Sorry I couldn't be more help.

Arlenarlena answered 9/6, 2022 at 20:19 Comment(1)
Thanks for the feedback. The queries written to the cache are data that only lives in the client-side cache and is queried using the @client directive (this is sort of like Apollo's answer to Redux. You can use it to write and query application state). The other two queries are network queries, hence they are in Apollo mocks. The ME query mock seems to work. If I remove it from the mocks, Apollo complains that it is missing. It's GET_INDIVIDUAL_DEMOGRAPHICS that blows up. I thought I had a Eureka moment this morning. This query uses type policies. I removed those fields but sadly, same deal.Ceremonial
A
0

It's probably a bit too late, but since I wasted some time on this issue it's worth posting for future readers.

It's most likely you are overwriting the mocked client, within your tested component.

So you have something like this in your component:

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

You'll have to wrap your original component (or higher order ones) with the ApolloProvider just as you are doing in your jest test, that way the Apollo library can overwrite that provider with your mocked one.

<ApolloProvider client={client}>
   <YourComponent/>
</ApolloProvider>
Acciaccatura answered 1/12, 2023 at 13:40 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.