Testing useSubscription apollo hooks with react

Testing the useSubscription hook I'm finding a bit difficult, since the method is omitted/not documented on the Apollo docs (at time of writing). Presumably, it should be mocked using the <MockedProvider /> from @apollo/react-testing, much like the mutations are in the examples given in that link.

Testing the loading state for a subscription I have working:

Component:

const GET_RUNNING_DATA_SUBSCRIPTION = gql`
  subscription OnLastPowerUpdate {
    onLastPowerUpdate {
      result1,
      result2,
    }
  }
`;

const Dashboard: React.FC<RouteComponentProps & Props> = props => {
  const userHasProduct = !!props.user.serialNumber;

  const [startGetRunningData] = useMutation(START_GET_RUNNING_DATA);

  const [stopGetRunningData] = useMutation(STOP_GET_RUNNING_DATA);

  useEffect(() => {
    startGetRunningData({
      variables: { serialNumber: props.user.serialNumber },
    });

    return () => {
      stopGetRunningData();
    };
  }, [startGetRunningData, stopGetRunningData, props]);

  const SubscriptionData = (): any => {
    const { data, loading } = useSubscription(GET_RUNNING_DATA_SUBSCRIPTION);

    if (loading) {
      return <Heading>Data loading...</Heading>;
    }

    const metrics = [];
    if (data) {
      console.log('DATA NEVER CALLED IN TEST!');
    }

    return metrics;
  };

  if (!userHasProduct) {
    return <Redirect to="/enter-serial" />;
  }

  return (
    <>
      <Header />
      <PageContainer size="midi">
        <Panel>
          <SubscriptionData />
        </Panel>
      </PageContainer>
    </>
  );
};

And a successful test of the loading state for the subscription:

import React from 'react';
import thunk from 'redux-thunk';
import { createMemoryHistory } from 'history';
import { create } from 'react-test-renderer';
import { Router } from 'react-router-dom';
import wait from 'waait';
import { MockedProvider } from '@apollo/react-testing';
import { Provider } from 'react-redux';

import configureMockStore from 'redux-mock-store';

import Dashboard from './Dashboard';

import {
  START_GET_RUNNING_DATA,
  STOP_GET_RUNNING_DATA,
  GET_RUNNING_DATA_SUBSCRIPTION,
} from './queries';

const mockStore = configureMockStore([thunk]);

const serialNumber = 'AL3286wefnnsf';

describe('Dashboard page', () => {
  let store: any;

  const fakeHistory = createMemoryHistory();

  const mocks = [
    {
      request: {
        query: START_GET_RUNNING_DATA,
        variables: {
          serialNumber,
        },
      },
      result: {
        data: {
          startFetchingRunningData: {
            startedFetch: true,
          },
        },
      },
    },
    {
      request: {
        query: GET_RUNNING_DATA_SUBSCRIPTION,
      },
      result: {
        data: {
          onLastPowerUpdate: {
            result1: 'string',
            result2: 'string'
          },
        },
      },
    },
    {
      request: {
        query: STOP_GET_RUNNING_DATA,
      },
      result: {
        data: {
          startFetchingRunningData: {
            startedFetch: false,
          },
        },
      },
    },
  ];

  afterEach(() => {
    jest.resetAllMocks();
  });

  describe('when initialising', () => {
    beforeEach(() => {
      store = mockStore({
        user: {
          serialNumber,
          token: 'some.token.yeah',
          hydrated: true,
        },
      });
      store.dispatch = jest.fn();
    });

    it('should show a loading state', async () => {
      const component = create(
        <Provider store={store}>
          <MockedProvider mocks={mocks} addTypename={false}>
            <Router history={fakeHistory}>
              <Dashboard />
            </Router>
          </MockedProvider>
        </Provider>,
      );

      expect(component.root.findAllByType(Heading)[0].props.children).toBe(
        'Data loading...',
      );
    });
  });
});

Adding another test to wait until the data has been resolved from the mocks passed in, as per the instructions on the last example from the docs for testing useMutation, you have to wait for it.

Broken test:

it('should run the data', async () => {
      const component = create(
        <Provider store={store}>
          <MockedProvider mocks={mocks} addTypename={false}>
            <Router history={fakeHistory}>
              <Dashboard />
            </Router>
          </MockedProvider>
        </Provider>,
      );
      await wait(0);
    });

Error the broken test throws:

No more mocked responses for the query: subscription OnLastPowerUpdate {

Dependencies:

    "@apollo/react-common": "^3.1.3",
    "@apollo/react-hooks": "^3.1.3",
    "@apollo/react-testing": "^3.1.3",

Things I've tried already:

  • react-test-renderer / enzyme / @testing-library/react
  • awaiting next tick
  • initialising the client in the test differently

Github repo with example:

https://github.com/harrylincoln/apollo-subs-testing-issue

Anyone out there able to help?


Solution 1:

The problem I can see here is that you're declaring the SubscriptionData component inside the Dashboard component so the next time the Dashboard component is re-rendered, the SubscriptionData component will be re-created and you'll see the error message:

No more mocked responses for the query: subscription OnLastPowerUpdate

I suggest that you take the SubscriptionData component out of the Dashboard component so it will be created only once

const SubscriptionData = (): any => {
  const { data, loading } = useSubscription(GET_RUNNING_DATA_SUBSCRIPTION);

  if (loading) {
    return <Heading>Data loading...</Heading>;
  }

  const metrics = [];
  if (data) {
    console.log('DATA NEVER CALLED IN TEST!');
  }

  return metrics;
};

const Dashboard: React.FC<RouteComponentProps & Props> = props => {
  const userHasProduct = !!props.user.serialNumber;

  const [startGetRunningData] = useMutation(START_GET_RUNNING_DATA);

  const [stopGetRunningData] = useMutation(STOP_GET_RUNNING_DATA);

  useEffect(() => {
    startGetRunningData({
      variables: { serialNumber: props.user.serialNumber },
    });

    return () => {
      stopGetRunningData();
    };
  }, [startGetRunningData, stopGetRunningData, props]);

  if (!userHasProduct) {
    return <Redirect to="/enter-serial" />;
  }

  return (
    <>
      <Header />
      <PageContainer size="midi">
        <Panel>
          <SubscriptionData />
        </Panel>
      </PageContainer>
    </>
  );
};

And for the tests you can try something like this:

let component;
it('should show a loading state', async () => {
      component = create(
        <Provider store={store}>
          <MockedProvider mocks={mocks} addTypename={false}>
            <Router history={fakeHistory}>
              <Dashboard />
            </Router>
          </MockedProvider>
        </Provider>,
      );
      expect(component.root.findAllByType(Heading)[0].props.children).toBe(
        'Data loading...',
      );

      await wait(0);
});

it('should run the data', async () => {
      expect(
        // another test here
        component.root...
      ).toBe();
});