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()
})
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,
}
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 themockCache
is correctly instantiated? If you manage to get past the error and mock the data I would also suggest to use RTLfindByText
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 thefindBy*
functions. – Baggywrinkle