9

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.

IanCZane
  • 600
  • 4
  • 21
  • 2
    I figured this out. It was related to my specific data. The mock that I supplied to the MockedProvider was a different shape than the GraphQL query, so it ended up returning nothing. Lesson: make sure the data you're outputting from the mock matches the expected GraphQL shape. – IanCZane May 20 '22 at 17:13
  • Sigh, ended on the same lesson: _Lesson: make sure **the data** you're outputting from the mock **matches** the **expected GraphQL shape**._ – edmundo096 Jun 06 '23 at 00:04

1 Answers1

3

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"
Lin Du
  • 88,126
  • 95
  • 281
  • 483