Skip to content

Mock Apollo Client in tests

How do we test Vue components with Apollo?

Currently, we mostly leverage mocking $apollo object on mount/shallowMount. This way we can set a loading state for a certain query and see how it's rendered or if $apollo.mutate is called with certain parameters. For checking query result, we assert it as a data property and for mutations we're able to mock resolved value. You can find more details on our FE GraphQL guide

Issue

This testing pattern is pretty much limited as we can't check what happens on query update or error hooks, and we are not able to check what changes mutation causes to Apollo cache.

Mocking Apollo Client to the rescue

We can overcome this issue if we mock an Apollo client similarly to how we mocked axios. I've built a proof of concept project using mock-apollo-client library to show what we could test with mocked client. Here is a spec for this component

mock-apollo-client allows us to create a mocked client where we can specify a return value for Apollo queries and mutations:

const heroListMock = {
  data: {
    allHeroes: [...],
  },
}


mockClient.setRequestHandler(
  allHeroesQuery,
  jest.fn().mockResolvedValue(heroListMock)
)

Using different handlers, we can check how component renders different responses or errors.

Below I'll describe a few examples from the POC.

Query loading state

If the query was added via apollo option (i.e. on component creation), we just need to mount a component and check if our loading state is rendered correctly (at this moment query is not resolved):

  it('renders a loading block when query is in progress', () => {
    createComponent()

    expect(wrapper.find('.test-loading').exists()).toBe(true)
    expect(wrapper.html()).toMatchSnapshot()
  })

Query resolved successfully

We would need to wait for the nextTick to have query resolved:

  it('renders a list of two heroes when query is resolved', async () => {
    createComponent()

    await wrapper.vm.$nextTick()

    expect(wrapper.find('.test-loading').exists()).toBe(false)
    expect(wrapper.html()).toMatchSnapshot()
    expect(wrapper.findAllComponents(VueHero)).toHaveLength(2)
  })

Query error

Here we change a handler to reject a Promise:

  it('renders error if query fails', async () => {
    createComponent({
      allHeroesQueryHandler: jest
        .fn()
        .mockRejectedValue(new Error('GraphQL error')),
    })

    // For some reason, when we reject the promise, it requires +1 tick to render an error
    await wrapper.vm.$nextTick()
    await wrapper.vm.$nextTick()

    expect(wrapper.find('.test-error').exists()).toBe(true)
  })

Mutations

My favorite part! The mocked client allows us to check cache changes and even check if the component is re-rendered after cache updates.

// This will be passed as a parameter to addHero mutation
const newHeroMock = {
  name: 'New Hero',
  github: '1000-contributions-a-day',
  twitter: 'new-hero',
  image: 'img.jpg',
}

// Successfull addHero mutation response
// IMPORTANT: always mock mutation responses with exactly the shape matching schema
// I.e. { data: { addHero: ... } }
const newHeroMockResponse = {
  data: {
    addHero: {
      __typename: 'Hero',
      id: '123',
      ...newHeroMock,
    },
  },
}

...

  it('adds a new hero to cache on addHero mutation', async () => {
    createComponent({}, { ...newHeroMock, dialog: true })

    // Waiting for query promise to resolve and populate data
    await wrapper.vm.$nextTick()

    wrapper.find('.test-submit').vm.$emit('click')
    
    expect(requestHandlers.addHeroMutationHandler).toHaveBeenCalledWith({
      hero: {
        ...newHeroMock,
      },
    })

    // We wait for mutation promise to resolve and then we check if new hero is added to cache
    await wrapper.vm.$nextTick()

    expect(
      mockClient.cache.readQuery({ query: allHeroesQuery }).allHeroes
    ).toHaveLength(3)

    // We wait for one more tick for component to re-render updated cache data
    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
    expect(wrapper.findAllComponents(VueHero)).toHaveLength(3)
  })

Advantages of switching patterns

  1. We improve test coverage as we can check cache update logic
  2. We unify our way of mocking handlers for queries and mutations

Disadvantages of switching patterns

  1. +1 dependency
  2. Setting handlers is verbose and we need to be very careful about mocked responses as they need to have correct structure (the same one GraphQL API returns). Otherwise, update hooks would fail.

What is the impact on our existing codebase?

First of all, I believe if we mock a client, we'll have integration tests, not unit ones as we're testing components working together with Apollo cache. I am not sure whether we need to rewrite existing unit tests or create integration tests for this case. Would be happy to hear your opinion on this!

Edited by Natalia Tepluhina