Some thoughts on `@apollo/client/testing`

Apollo Client is my go-to GraphQL client implementation because it’s so fully-featured. For the most part, this one library covers my state management, network connections, and can even “decorate” data like dates & URLs so I can use more sophisticated tooling for dealing with them.

There’s one major flaw that I’d like to address, though. Apollo’s testing story is confusing at best and actively harmful to applications at worst. I know this is turning into a “…considered harmful” post, but I’m serious: I’ve never used a mocking library that let you shoot yourself in the foot quite as much as @apollo/client/testing, to the point where I actually check $NODE_ENV === "test" in the implementation just to get around its bullshit so I can actually test what I want to test. Whenever you’re using $NODE_ENV to bypass actual implementation code (which can break!) for the sake of your test framework, something is extremely wrong.

Mocking

In order to test a single component in my application, I need a mocks array that is over 300 lines long. Most of this code is totally meaningless, it’s just there to satisfy the “loading state” because otherwise Apollo throws errors since it can’t find any more mocks.

Here’s an example of what that looks like:

  {
    request: {
      query: CommunityNameDocument,
      variables: { id },
    },
    result: {
      data: {
        community: {
          name: 'Community Name',
        },
      },
    },
  },
  {
    request: {
      query: KickedFromRoomDocument,
      variables: { room },
    },
    result: {
      data: {},
    },
  },
  {
    request: {
      query: KickedFromCommunityDocument,
      variables: { community: id },
    },
    result: {
      data: {},
    },
  },
  {
    request: {
      query: KickedFromCommunityDocument,
      variables: { community: id },
    },
    result: {
      data: {},
    },
  },
  {
    request: {
      query: NewMessageDocument,
      variables: {},
    },
    result: {
      data: {},
    },
  },
  {
    request: {
      query: NewMessageDocument,
      variables: { room },
    },
    result: {
      data: {},
    },
  },
  {
    request: {
      query: AddStreamToRoomDocument,
      variables: { id: room, url: 'https://twitch.tv/nerdstreet' },
    },
    result: {
      data: {
        addStreamToRoom: {
          stream: {
            channel: 'nerdstreet',
          },
        },
      },
    },
  },

Notice how much duplication there is? Those KickedFrom*Document operations are subscriptions, and in all honesty I’m not testing their functionality at all in this component, yet I still need hundreds of lines of bullshit code just to satisfy Apollo’s weird requirements. In this particular instance, I feel that MockedProvider should give me the option to disable subscriptions. If I’m not testing subscriptions, all they do is get in the way.

Success vs Loading State

Another huge issue that really threw me for a loop in the beginning (and is probably contributing to the above problem!) is that the “loading” state is actually the default out-of-box state that occurs in the test. This means that newcomers who didn’t read the one line in the docs that happens to specify this (buried WAY DEEP in the middle of the page too!) will be completely confused as to why their test framework doesn’t work.

Also, this line? Doesn’t do anything.

    await new Promise((resolve) => setTimeout(resolve, 0))

…except give me a bunch of cryptic errors in my tests.

I think this is actually the biggest problem with Apollo Testing, and changing render() to return a promise and/or making the success state the default behavior is (in my opinion) the most significant change that can be made in order to improve the testing experience. When you sit down and write tests, you’re trying to express what happens when something occurs in the app, and hopefully from the perspective of a user. I don’t want to have to think about Apollo’s implementation details to write tests for my application, otherwise the testing framework is just useless.

The Cache

The cache is on by default in tests. Why?

This is how I have to set up a MockedProvider in order for things to actually work:

      <MockedProvider
        mocks={mocks}
        addTypename={false}
        defaultOptions={{ watchQuery: { fetchPolicy: 'no-cache' } }}
      >

I just find all of this to be incredibly confusing, and it’s no wonder why people are afraid to test Apollo apps. Testing doesn’t even work without the last two props most of the time, so why can’t those just be default?

1 Like

I agree that the built in testing solution is a bit tedious to use for more complex apps. I personally don’t use MockedProvider anywhere in my tests, I prefer an approach like this one: Testing Apollo Components using react-testing-library
Might not solve all of your problems, but I think it’s worth giving a shot. This approach involves passing your whole backend schema to the mock generator so that it can automatically generate responses of the correct shape without you having to specify any response data. But you can still specify custom response data if you want to, or you can mix and match! For testing loading states, you can return a Promise from the mock resolver, then test your loading state. Then you can resolve your Promise and test your completed state.

2 Likes

I used react-testing-library instead of the examples in the apollo docs which use react-test-renderer.

This is the library that is actually recommended by the team that builds react and is included in apps built using create-react-app. I agree that the mocks are pretty explicit. You can create a function to generate the code to reduce the amount that you are creating. Passing in an array of request and response values to generate the mocks. This data can be all in its own file if you want to separate it from your testing logic.

  import { MockedProvider, MockedResponse } from '@apollo/client/testing';

  const mocks: MockedResponse[] = generateMocks([{q:QUERY_GET_STUFF,v:{id:21},r:{title:'test response'}]);