Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 703
Harry Lincoln Avatar asked Apr 29 '20 14:04

Harry Lincoln


People also ask

Are react Apollo hooks fully typed?

React Apollo's useQuery, useMutation and useSubscription hooks are fully typed, and Generics can be used to type both incoming operation variables and GraphQL result data. React Apollo Hook options and result types are listed in the Hooks API section of the docs. You can find a typed example of each Hook below.

How do I Write Result types in react Apollo?

In these docs however, we will be writing result types manually. React Apollo's useQuery, useMutation and useSubscription hooks are fully typed, and Generics can be used to type both incoming operation variables and GraphQL result data. React Apollo Hook options and result types are listed in the Hooks API section of the docs.

How to mock a subscription in react testing?

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:

When to perform a query refetch in Apollo client?

If true, perform a query refetch if the query result is marked as being partial, and the returned data is reset to an empty Object by the Apollo Client QueryManager (due to a cache miss). The default value is false for backwards-compatibility's sake, but should be changed to true for most use-cases. An ApolloClient instance.


1 Answers

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();
});
like image 107
Fraction Avatar answered Oct 29 '22 04:10

Fraction