Some background: I've got a component that immediately calls a useQuery hook upon loading. While that query is running, I spin a loading spinner. Once it completes I render stuff based on the data.
I've added a useEffect
hook that watches the result of the query and logs the data, which is how I observed this issue.
To simplify things, it works like this:
export default function MyComponent(props: ???) {
const result = useQuery(INITIAL_DATA_QUERY, { variables: { id: 1 } });
React.useEffect(() => console.log(JSON.stringify({
loading: result.loading,
data: result.data,
error: result.error,
})), [result]);
if (result.loading) return <LoadingScreen message="Fetching data..."/>;
else if (result.error) return <ErrorPage/>
else return <Stuff info={result.data}> // omitted because it's unimportant to the issue
}
When I run this component in the wild, everything works exactly as expected. It hits the endpoint with GraphQL through Apollo, makes rendering decisions based on the result, etc.
When I try to mock the request out though, the result.data
and result.error
fields never change, even though the result.loading
field does. I am using react-testing-library
to run the tests.
My tests look like this:
it("should load the data then render the page", () => {
const mocks = [{
request: {
query: INITIAL_DATA_QUERY,
variables: { id: 1 },
},
newData: jest.fn(() => ({
data: {
firstName: "Joe",
lastName: "Random",
}
}))
}];
const mockSpy = mocks[0].newData;
render(
<MockedProvider mocks={mocks} addTypename={false}>
<MyComponent/>
</MockedProvider>
)
// Is it a loading view
expect(result.asFragment()).toMatchSnapshot(); // Passes just fine, and matches expectations
// Wait until the mock has been called once
await waitFor(() => expect(mockSpy).toHaveBeenCalled(1)) // Also passes, meaning the mock was called
// Has the page rendered once the loading mock has finished
expect(result.asFragment()).toMatchSnapshot(); // Passes, but the page has rendered without any of the data
})
The problem is this: when I run this test, all three of those tests pass as expected, but in the final fragment the data in my rendered component is missing. I am sure the mock is being called because I've added some logger statements to check.
The really confusing part are the loading
, data
, and error
values as the mock is called. I have a useEffect
statement logging their values when any of them change, and when I run the test, the output looks like this:
{ loading: true, data: undefined, error: undefined }
{ loading: false, data: undefined, error: undefined }
This means that the hook is being called and loading begins, but once loading ends whatever happened during loading neither returned any data nor generated any errors.
Does anybody know what my problem here might be? I've looked at it eight ways to Sunday and can't figure it out.
I mocked the result using the result
field.
the
result
field can be a function that returns a mocked response after performing arbitrary logic
It works fine for me.
MyComponent.test.tsx
:
import { gql, useQuery } from '@apollo/client';
import { useEffect } from 'react';
export const INITIAL_DATA_QUERY = gql`
query GetUser($id: ID!) {
user(id: $id) {
firstName
lastName
}
}
`;
export default function MyComponent(props) {
const result = useQuery(INITIAL_DATA_QUERY, { variables: { id: 1 } });
useEffect(
() =>
console.log(
JSON.stringify({
loading: result.loading,
data: result.data,
error: result.error,
}),
),
[result],
);
if (result.loading) return <p>Fetching data...</p>;
else if (result.error) return <p>{result.error}</p>;
else return <p>{result.data.user.firstName}</p>;
}
MyComponent.test.tsx
:
import { render, waitFor } from '@testing-library/react';
import MyComponent, { INITIAL_DATA_QUERY } from './MyComponent';
import { MockedProvider } from '@apollo/client/testing';
describe('68732957', () => {
it('should load the data then render the page', async () => {
const mocks = [
{
request: {
query: INITIAL_DATA_QUERY,
variables: { id: 1 },
},
result: jest.fn().mockReturnValue({
data: {
user: {
lastName: 'Random',
firstName: 'Joe',
},
},
}),
},
];
const mockSpy = mocks[0].result;
const result = render(
<MockedProvider mocks={mocks} addTypename={false}>
<MyComponent />
</MockedProvider>,
);
expect(result.asFragment()).toMatchSnapshot();
await waitFor(() => expect(mockSpy).toBeCalledTimes(1));
expect(result.asFragment()).toMatchSnapshot();
});
});
test result:
PASS src/stackoverflow/68732957/MyComponent.test.tsx
68732957
✓ should load the data then render the page (58 ms)
console.log
{"loading":true}
at src/stackoverflow/68732957/MyComponent.tsx:18:15
console.log
{"loading":false,"data":{"user":{"firstName":"Joe","lastName":"Random"}}}
at src/stackoverflow/68732957/MyComponent.tsx:18:15
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 2 passed, 2 total
Time: 0.736 s, estimated 1 s
MyComponent.test.tsx.snap
:
// Jest Snapshot v1
exports[`68732957 should load the data then render the page 1`] = `
<DocumentFragment>
<p>
Fetching data...
</p>
</DocumentFragment>
`;
exports[`68732957 should load the data then render the page 2`] = `
<DocumentFragment>
<p>
Joe
</p>
</DocumentFragment>
`;
package versions:
"@testing-library/react": "^11.1.0",
"react": "^17.0.1",
"@apollo/client": "^3.4.7",
"graphql": "^15.4.0"