Search code examples
javascriptreactjsmockingapolloreact-testing-library

Apollo MockedProvider always returns undefined for data field, even after loading


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.


Solution

  • 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"