How to do unit test for watchquery in react

I have a difficult time to unit test watch query. I tried mockWatchQuery and subscribeAndCount, which causes test timeout. It seems the subscription never receive any data. Please, provide a guidance how to mock and test that. Thank you!

Hey @Eric_Zhang :wave:

Would you mind pasting a sample of a test that times out on you? I’d be happy to give some advice.

Thanks Jerel,
I have a custom hooks look like

const useCustomHook = () => {
  const {data: listItems} = useListItemsQuery(...);
  const [result, setResult] = useState(null);

  useEffect(() => {
      const subscriptions = new Set<ObservableSubscription>();
      const completedResult = [];
  
       if (listItems && listItems.length > 0) {
            listItems.forEach((item) => {
                const observable = client.watchQuery({
                   query: GetItemDocument
                });
                const subscription = observable.subscribe({
                     next: (res) => {
                        if(res.status === 'pending') {
                             observable.startPolling(POLL_INTERVAL);
                         } else {
                             observable.stopPolling();
                            completedResult = completedResult.concat(res);
                             subscription.unsubscribe();
                             subscriptions.delete(subscription);

                             if (subscriptions.size === 0) {
                                setResult(completedResult);
                             }
                         }
                     }
                });
                subscriptions.add(subscription);
            })
       }

      return () => {
        subscriptions.forEach((subscription) => subscription.unsubscribe());
        subscriptions.clear();
      };
   }, [listItems])

   return {result};
}

Here is my unit test which timed out.

import { itAsync, MockedResponse, subscribeAndCount } from '@apollo/client/testing';
import mockWatchQuery from '@apollo/client/testing/core/mocking/mockWatchQuery';

itAsync('should return list item if all items are complete.', (resolve, reject) => {
    getHookResult( // helper func for renderHook from '@testing-library/react'
      () =>
        useCustomHook(...),
      [listItemsSuccessMock],
    );

    const observable: ObservableQuery<any> = mockWatchQuery(
      {
        request: {
          query: GetItemDocument,
          variables: ...,
        },
        result: ..., // before polling
      },
      {
        request: {
          query: GetItemDocument,
          variables: ...,
        },
        result:  ... // after polling
      },
    );

    subscribeAndCount(reject, observable, (handleCount, result) => {
      if (handleCount === 1) {
        expect(result.data).toEqual(beforePollingdata);
        expect(result.loading).toBe(false);
        observable.setOptions({ query: ListWorkflowRunsDocument, pollInterval: 0 });
      }

      if (handleCount === 2) {
        expect(result.loading).toBeTruthy();
      }

      if (handleCount === 3) {
        expect(result.loading).toBe(false);
        expect(result.data).toEqual(afterPollingdata);
        resolve();
      }
    });
  });

Is the subscribeAndCount only for testing observableQuery, but not how we test ObservableSubscription.

Hey @Eric_Zhang

Thanks for those code snippets! I’m curious, are you trying to test the observables inside your hook? It looks like you are testing against an created from your test (the one created via mockWatchQuery) that is completely disconnected from the observables created inside your hook, so even if you were able to get this specific test to work, I’d argue this could give you a false positive (by that I mean that if you can comment out your call to your hook and see your test still pass, that should give you some pause. Something I think your current implementation would do if it was currently working.)

Instead of trying to mock at the observable level, instead I recommend you try and mock at the network level. Any method in Apollo Client that would trigger a network will request go through your link chain. In your development and production environments, your link chain typically uses an HttpLink to make network requests to your GraphQL server. In a testing environment however, you’ll generally want to use a MockLink to simulate the network requests in order to avoid actually hitting the network (this is what MockedProvider uses under the hood).

I’m not sure how you’re getting access to the client in your custom hook, but in order to properly mock the network requests, you’ll need to make sure that client uses MockLink instead of HttpLink. You can pass your mocks directly to that link and it will simulate each network request. This might look something like this:

import { MockLink } from '@apollo/client/testing';

const mocks = [
  {
    request: {
      query: GetItemDocument,
      variables: ...,
    },
    result: ..., // before polling
  },
  {
    request: {
      query: GetItemDocument,
      variables: ...,
    },
    result:  ... // after polling
  },
];

const client = new ApolloClient({
  cache: new InMemoryCache(),

  // use `MockLink` in your test env instead `HttpLink` in your link chain
  link: new MockLink(mocks),
});

I think you’ll find it much easier to test your hook this way as it doesn’t require you to try and mock the observables created in your custom hook from watchQuery.

Assuming your custom hook returns the result from your useState, and that you can get access to that result from your getHookResult helper, I’d recommend using Testing Library’s waitFor method to assert on the result of that hook. Testing this way allows you to avoid testing the implementation details of your hook and instead test on the inputs/outputs.

This might look something like this (some code is made up since I don’t know the extent of your setup)

const mocks = [...]

// not sure how you hook up client to your hook, but make sure it uses this one
const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: new MockLink(mocks)
});

const result = getHookResult(() => useCustomHook(...))

await waitFor(() => {
  expect(result).toEqual([
    // whatever shape your result should be
  ]);
});

As a note of advice, I’d also recommend testing error states, for example, if one of your items queries fails. Make sure your hook can handle this.


One last thing I’d like to mention here. I see you’re using some of our internal helper functions in your test. While we do export these, I’ll just warn you that these undocumented utilities may change or break at any given time.

For example, I’d like to completely deprecate our itAsync function and use plain async/await in our tests, which means this function will disappear. I don’t have a timetable on this, but we want to be free to remove it at any time. If you rely too heavy on some of these undocumented utilities, you may find find these break over time.

This isn’t to say you can’t or shouldn’t use them, just more of a Use At Your Own Risk :tm: instead :smile:

Let me know if you have any more questions! Hopefully this helps steer you in the right direction.

1 Like

Thanks, It is really helpful. One more question:

In the getHookResult , it is using MockedProvider. All of my code is under ApolloProvider,, I am using const client = useApolloClient(); to grab my client instance. From you example, I need create a new client instance. Will it make my test handled by the new client instance here?

@Eric_Zhang ahh that makes things much easier. I didn’t see the useApolloClient usage in your original example so I wasn’t sure how you were getting access to client.

In that case, don’t follow what I did by creating the client in your test. MockedProvider will do that for you. Just make sure you’re able to pass the mocks from your test to your getHookResult function so that they can be used with MockedProvider.

1 Like

It works! Thanks a lot.

1 Like

Awesome! Glad to hear!