Testing Vue+Apollo: 2020 edition
Tags: vue, graphql, testing
Published: 2020-06-25 12:53:28

Almost two years ago I started my [dev.to](https://dev.to/) journey with the article about [unit testing Vue + Apollo combination](https://dev.to/n_tepluhina/testing-vue-apollo-components-with-jest-3gj3). During this time I had multiple requests about mocking an Apollo client and including it to the equation - just like React does with [@apollo/react-testing](https://www.apollographql.com/docs/react/api/react-testing/) library. This would allow us to test queries and mutations hooks as well as cache updates. There were many attempts from my side to mock a client and finally I am ready to share some examples.

> Tests in this article are rather _integration_ that _unit_ tests because we test component and Apollo Client in combination

> This article assumes some prior knowledge of [Vue.js](https://vuejs.org/), [Apollo Client](https://www.apollographql.com/client/) and unit testing with [Jest](https://jestjs.io/) and [Vue Test Utils](https://vue-test-utils.vuejs.org/)

## What we're going to test

I decided to go with the [same project](https://github.com/NataliaTepluhina/vue-graphql-presentation/tree/testing-mock-client) I was testing in the scope of the previous article. Here, we have a single huge [App.vue](https://github.com/NataliaTepluhina/vue-graphql-presentation/blob/testing-mock-client/src/App.vue) component that contains logic to fetch a list of people from Vue community, add a new member there or delete an existing one.

In this component, we have one query:

```js
// Here we're fetching a list of people to render

apollo: {
  allHeroes: {
    query: allHeroesQuery,
    error(e) {
      this.queryError = true
    },
  },
},
```

And two mutations (one to add a new hero and one to delete an existing one). Their tests are pretty similar, that's why we will cover only 'adding a new hero' case in the article. However, if you want to check the test for deleting hero, here is the [source code](https://github.com/NataliaTepluhina/vue-graphql-presentation/blob/testing-mock-client/tests/unit/App.spec.js#L187)

```js
// This is a mutation to add a new hero to the list
// and update the Apollo cache on a successful response

this.$apollo
  .mutate({
    mutation: addHeroMutation,
    variables: {
      hero,
    },
    update(store, { data: { addHero } }) {
      const data = store.readQuery({ query: allHeroesQuery });
      data.allHeroes.push(addHero);
      store.writeQuery({ query: allHeroesQuery, data });
    },
  })
  .finally(() => {
    this.isSaving = false;
  });
```

We would need to check that

- the component renders a `loading` state correctly when the query for _Vue heroes_ is in progress;
- the component renders a response correctly when the query is resolved (an 'empty state' with 0 heroes should be tested too);
- the component renders an error message if we had an error on the query;
- the component sends `addHero` mutation with correct variables, updates a cache correctly on successful response and re-renders a list of heroes;

Let's start our journey!

## Setting up a unit test with `createComponent` factory

Honestly, this section is not specific to Apollo testing, it's rather a useful technique to prevent repeating yourself when you mount a component. Let's start with creating an `App.spec.js` file, importing some methods from vue-test-utils and adding a factory for mounting a component

```js
// App.spec.js

import { shallowMount } from '@vue/test-utils'
import AppComponent from '@/App.vue'

describe('App component', () => {
  let wrapper

  const createComponent = () => {
    wrapper = shallowMount(AppComponent, {})
  };

  // We want to destroy mounted component after every test case
  afterEach(() => {
    wrapper.destroy()
  })
})
```

Now we can just call a `createComponent` method in our tests! In the next section, we will extend it with more functionality and arguments.

## Mocking Apollo client with handlers

First of all, we need to mock an Apollo Client so we'd be able to specify handlers for queries and mutations. We will use [mock-apollo-client](https://www.npmjs.com/package/mock-apollo-client) library for this:

```bash
npm --save-dev mock-apollo-client
## OR
yarn add -D mock-apollo-client
```

Also, we would need to add `vue-apollo` global plugin to our mocked component. To do so, we need to create a local Vue instance and call `use()` method to add VueApollo to it:

```js
// App.spec.js
import { shallowMount, createLocalVue } from '@vue/test-utils'
import AppComponent from '@/App.vue'
import VueApollo from 'vue-apollo'

const localVue = createLocalVue()
localVue.use(VueApollo)

...

const createComponent = () => {
  wrapper = shallowMount(AppComponent, {
    localVue
  });
};
```

Now we need to create a mock client and provide it to the mocked component:

```js
...
import { createMockClient } from 'mock-apollo-client'
...

describe('App component', () => {
  let wrapper
  // We define these variables here to clean them up on afterEach
  let mockClient
  let apolloProvider

  const createComponent = () => {
    mockClient = createMockClient()
    apolloProvider = new VueApollo({
      defaultClient: mockClient,
    })

    wrapper = shallowMount(AppComponent, {
      localVue,
      apolloProvider,
    })
  }

  afterEach(() => {
    wrapper.destroy()
    mockClient = null
    apolloProvider = null
  })
})
```

Now we have `$apollo` property in our mounted component and we can write the first test just to ensure we didn't fail anywhere:

```js
it('renders a Vue component', () => {
  createComponent()

  expect(wrapper.exists()).toBe(true)
  expect(wrapper.vm.$apollo.queries.allHeroes).toBeTruthy()
});
```

Great! Let's add the first handler to our mocked client to test the `allHeroes` query

## Testing successful query response

To test a query, we would need to define a _query response_ that we'll have when the query is resolved. We can do this with the `setRequestHandler` method of `mock-apollo-client`. To make our tests more flexible in the future, we will define an object containing default request handlers plus any additional handlers we want to pass to `createComponent` factory:

```js
let wrapper
let mockClient
let apolloProvider
let requestHandlers

const createComponent = (handlers) => {
  mockClient = createMockClient()
  apolloProvider = new VueApollo({
    defaultClient: mockClient,
  })

  requestHandlers = {
    ...handlers,
  }
  ...
}
```

Let's also add a new constant at the top of the test file with the mocked query response:

```js
// imports are here
const heroListMock = {
  data: {
    allHeroes: [
      {
        github: 'test-github',
        id: '-1',
        image: 'image-link',
        name: 'Anonymous Vue Hero',
        twitter: 'some-twitter',
      },
      {
        github: 'test-github2',
        id: '-2',
        image: 'image-link2',
        name: 'another Vue Hero',
        twitter: 'some-twitter2',
      },
    ],
  },
};
```

> It's _very_ important to mock here exactly the same structure you expect to get from your GraphQL API (including root `data` property)! Otherwise your test will fail miserably 😅

Now we can define a handler for `allHeroes` query:

```js
requestHandlers = {
  allHeroesQueryHandler: jest.fn().mockResolvedValue(heroListMock),
  ...handlers,
};
```

...and add this handler to our mocked client

```js
import allHeroesQuery from '@/graphql/allHeroes.query.gql'
...

mockClient = createMockClient()
apolloProvider = new VueApollo({
  defaultClient: mockClient,
})

requestHandlers = {
  allHeroesQueryHandler: jest.fn().mockResolvedValue(heroListMock),
  ...handlers,
}

mockClient.setRequestHandler(
  allHeroesQuery,
  requestHandlers.allHeroesQueryHandler
)
```

Now, when the mounted component in the test will try to fetch `allHeroes`, it will get the `heroListMock` as a response - i.e. when the query is _resolved_. Until then, the component will show us a loading state.

In our `App.vue` component we have this code:

```html
<h2 v-if="queryError" class="test-error">
  Something went wrong. Please try again in a minute
</h2>
<div v-else-if="$apollo.queries.allHeroes.loading" class="test-loading">
  Loading...
</div>
```

Let's check if `test-loading` block is rendered:

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

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

Great! Loading state is covered, now it's a good time to see what happens when the query is resolved. In Vue tests this means we need to wait for the _next tick_:

```js
import VueHero from '@/components/VueHero'
...

it('renders a list of two heroes when query is resolved', async () => {
  createComponent()
  // Waiting for promise to resolve here
  await wrapper.vm.$nextTick()

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

## Changing a handler to test an empty list

On our `App.vue` code we also have a special block to render when heroes list is empty:

```html
<h3 class="test-empty-list" v-if="allHeroes.length === 0">
  No heroes found 😭
</h3>
```

Let's add a new test for this and let's now _pass a handler_ to override a default one:

```js
it('renders a message about no heroes when heroes list is empty', async () => {
  createComponent({
    // We pass a new handler here
    allHeroesQueryHandler: jest
      .fn()
      .mockResolvedValue({ data: { allHeroes: [] } }),
  })

  await wrapper.vm.$nextTick()

  expect(wrapper.find('.test-empty-list').exists()).toBe(true);
});
```

As you can see, our mocked handlers are flexible - we can change them on different tests. There is some space for further optimization here: we could change `requestHandlers` to have queries as keys and iterate over them to add handlers, but for the sake of simplicity I won't do this in the article.

## Testing query error

Our application also renders an error in the case of failed query:

```js
apollo: {
  allHeroes: {
    query: allHeroesQuery,
    error(e) {
      this.queryError = true
    },
  },
},
```

```html
<h2 v-if="queryError" class="test-error">
  Something went wrong. Please try again in a minute
</h2>
```

Let's create a test for the error case. We would need to replace mocked resolved value with the rejected one:

```js
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)
})
```

## Testing a mutation to add a new hero

Queries are covered! What about mutations, are we capable to test them properly as well? The answer is `YES`! First, let's take a look at our mutation code:

```js
const hero = {
  name: this.name,
  image: this.image,
  twitter: this.twitter,
  github: this.github,
};
...
this.$apollo
  .mutate({
    mutation: addHeroMutation,
    variables: {
      hero,
    },
    update(store, { data: { addHero } }) {
      const data = store.readQuery({ query: allHeroesQuery });
      data.allHeroes.push(addHero);
      store.writeQuery({ query: allHeroesQuery, data });
    },
  })
```

Let's add two new constants to our mocks: the first one for the `hero` variable passed as a mutation parameter, and a second one - for the successful mutation response

```js
...
import allHeroesQuery from '@/graphql/allHeroes.query.gql'
import addHeroMutation from '@/graphql/addHero.mutation.gql'

const heroListMock = {...}

const heroInputMock = {
  name: 'New Hero',
  github: '1000-contributions-a-day',
  twitter: 'new-hero',
  image: 'img.jpg',
}

const newHeroMockResponse = {
  data: {
    addHero: {
      __typename: 'Hero',
      id: '123',
      ...heroInputMock,
    },
  },
}
```

> Again, be very careful about mocking a response! You would need to use a correct typename to match the GraphQL schema. Also, your response shape should be the same as a real GraphQL API response - otherwise, you will get a hiccup on tests

Now, we add a mutation handler to our handlers:

```js
requestHandlers = {
  allHeroesQueryHandler: jest.fn().mockResolvedValue(heroListMock),
  addHeroMutationHandler: jest.fn().mockResolvedValue(newHeroMockResponse),
  ...handlers,
};

mockClient.setRequestHandler(
  addHeroMutation,
  requestHandlers.addHeroMutationHandler
);
```

It's time to start writing a mutation test! We will skip testing loading state here and we will check the successful response right away. First, we would need to modify our `createComponent` factory slightly to make it able to set component `data` (we need this to 'fill the form' to have correct variables sent to the API with the mutation):

```js
const createComponent = (handlers, data) => {
  ...
  wrapper = shallowMount(AppComponent, {
    localVue,
    apolloProvider,
    data() {
      return {
        ...data,
      };
    },
  });
};
```

Now we can start creating a mutation test. Let's check if the mutation is actually called:

```js
it('adds a new hero to cache on addHero mutation', async () => {
  // Open the dialog form and fill it with data
  createComponent({}, { ...heroInputMock, dialog: true })

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

  // Submit the form to call the mutation
  wrapper.find('.test-submit').vm.$emit("click")

  expect(requestHandlers.addHeroMutationHandler).toHaveBeenCalledWith({
    hero: {
      ...heroInputMock,
    },
  });
});
```

Next step is to wait until mutation is resolved and check if it updated Apollo Client cache correctly:

```js
it('adds a new hero to cache on addHero mutation', async () => {
  ...
  expect(requestHandlers.addHeroMutationHandler).toHaveBeenCalledWith({
    hero: {
      ...heroInputMock,
    },
  });

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

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

Finally, we can wait for one more tick so Vue could re-render the template and we will check the actual rendered result:

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

  await wrapper.vm.$nextTick()

  wrapper.find('.test-submit').vm.$emit("click")

  expect(requestHandlers.addHeroMutationHandler).toHaveBeenCalledWith({
    hero: {
      ...heroInputMock,
    },
  })

  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);
});
```

That's it! We also can mock mutation error the same way as we did for the query error but I believe this article is already long and boring enough 😅

You can find the full source code for the test [here](https://github.com/NataliaTepluhina/vue-graphql-presentation/blob/testing-mock-client/tests/unit/App.spec.js)