Almost two years ago I started my dev.to journey with the article about unit testing Vue + Apollo combination. 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 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, Apollo Client and unit testing with Jest and Vue Test Utils
What we're going to test
I decided to go with the same project I was testing in the scope of the previous article. Here, we have a single huge 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:
// 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
// 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
// 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 library for this:
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:
// 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:
...
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:
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:
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:
// 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:
requestHandlers = {
allHeroesQueryHandler: jest.fn().mockResolvedValue(heroListMock),
...handlers,
};
...and add this handler to our mocked client
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:
<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:
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:
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:
<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:
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:
apollo: {
allHeroes: {
query: allHeroesQuery,
error(e) {
this.queryError = true
},
},
},
<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:
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:
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
...
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:
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):
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:
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:
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:
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
Top comments (6)
Amazing topic, thank you for your work. Two questions:
What about vue-composable code and testing of it(currently in internet there are no docs and exapmles about that :(
What about vue-testing-library, currently it is also not so much examples. Would be amazing if you are also provide some examples :)
If you give any comments on that will be very appreciated :)
1) If we're speaking about Vue Apollo composables, we don't test them in isolation. It will be the same component test with @vue/apollo-composable. If we're speaking about "how to test an abstract standalone custom composable" - it's a bit of a different question and I would need a better research to answer it (currently, we're still using Vue 2 so I can't have a lot of hands on experience with composables in prod). I'll try to look into it.
2) I prefer pure vue-test-utils over vue-testing-library but it's just a personal preference
To be honest I came to Vue from react world and especially from Apollo client there. For me useQuery is kind of hook and work the same way as in react(declarative), so I expected to test it also easily that I did before. I also think that using this approach is the future for Vue(for react Apollo client it is already a standard) itself because it is much simpler, what disappointed me is a lack of tools and documentation , but thank you ๐ for people like you who provide this information like this post.
I expected that I wrapped my Vue component with MockProvider, provide some mock data and I am done. But now what I see that is so many complexity involved and I have not two many options left and all of them involved so many boilerplate in code and also in tests(instead of two lines with useQuery and useResult)
So if you would give some advices how to keep in code this two methods and also write tests for this components I would really appreciate that ๐
Amazing topic, thank you. But how are you fixing warnings about local queries and mutations?
Found @client directives in a query but no ApolloClient resolvers were specified. This means ApolloClient local resolver handling has been disabled, and @client directives will be passed through to your link chain.
I have this error when I'm using @client directive on my local queries
You need to add local resolvers when we createMockClient.
createMockClient({
resolvers: {
Mutation: {
Your_Mutation_Resolvers_Here
},
Query: {
Your_Query_Resolvers_Here
},
}
})
And do not set Request handlers, instead use writeQuery() to initialize the cache.
mockclient.cache.writeQuery({ ... })
Thank you for this well-written and comprehensive article. Very helpful!