Recently I've been working on some tests for Vue single-file components with vue-apollo queries and mutations. Unfortunately, there are not so many guides on the topic so I decided to share my experience. This article doesn't pretend to be a best-practice but I hope it will help people to start testing GraphQL + Apollo in Vue with Jest.
vue-apollo is a library that integrates Apollo in Vue components with declarative queries
Project overview
I added vue-apollo tests to my simple demo application. It contains an App.vue
component with one query for fetching the list of Vue core team members and two mutations: one to create a new member entry and another to delete it. Full GraphQL schema could be found in apollo-server/schema.graphql
file.
For component unit testing I used Jest and vue-test-utils.
If you have a look at tests
folder, you might notice project already had a basic test for App.vue
:
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuetify from 'vuetify';
import App from '../../src/App';
describe('App', () => {
let localVue;
beforeEach(() => {
localVue = createLocalVue();
localVue.use(Vuetify, {});
});
test('is a Vue instance', () => {
const wrapper = shallowMount(App, { localVue });
expect(wrapper.isVueInstance()).toBeTruthy();
});
});
This project uses Vuetify, so I added it to localVue
to prevent warnings about its custom components. Also, there is a simple check if component is a Vue instance. Now it's time to write some vue-apollo-related tests!
Simple tests
At first, when I was searching for any pointers about how to test vue-apollo queries and mutations, I found this comment by vue-apollo author, Guillaume Chau
Comment for #244
I recommend to use vue test-utils if you don't already. Then you have to mock everything related to apollo. If you have queries, just use wrapper.setData
. If you have mutations, mock them like this:
const mutate = jest.fn()
const wrapper = mount(MyComponent, {
mocks: {
$apollo: {
mutate,
},
},
})
// ...
expect(mutate).toBeCalled()
So I decided to start testing my component using this advice. Let's create a new test case:
test('displayed heroes correctly with query data', () => {
const wrapper = shallowMount(App, { localVue });
});
After this we need to save a correct response to the wrapper data and check if component renders correctly. To get the response structure we can check a query in project schema:
type VueHero {
id: ID!
name: String!
image: String
github: String
twitter: String
}
type Query {
allHeroes: [VueHero]
}
So allHeroes
query shoud return an array of VueHero
entries and every single field type is specified. Now it's easy to mock the data inside our wrapper:
wrapper.setData({
allHeroes: [
{
id: 'some-id',
name: 'Evan You',
image:
'https://pbs.twimg.com/profile_images/888432310504370176/mhoGA4uj_400x400.jpg',
twitter: 'youyuxi',
github: 'yyx990803',
},
],
});
Awesome, our data is mocked! Now it's time to check if it's rendered correctly. For this purpose I used a Jest snapshot feature: a test expects that component will match the given snapshot. Final test case looks like this:
test('displayed heroes correctly with query data', () => {
const wrapper = shallowMount(App, { localVue });
wrapper.setData({
allHeroes: [
{
id: 'some-id',
name: 'Evan You',
image:
'https://pbs.twimg.com/profile_images/888432310504370176/mhoGA4uj_400x400.jpg',
twitter: 'youyuxi',
github: 'yyx990803',
},
],
});
expect(wrapper.element).toMatchSnapshot();
});
If you run it a couple of times, you will see test passes (nothing surprising here, with a given set of data component renders in the same way every time). This is how the heroes grid in the snapshot looks like at this moment:
<v-layout-stub
class="hero-cards-layout"
tag="div"
wrap=""
>
<v-flex-stub
class="hero-cards"
md3=""
tag="div"
xs12=""
>
<v-card-stub
height="100%"
tag="div"
>
<v-card-media-stub
height="250px"
src="https://pbs.twimg.com/profile_images/888432310504370176/mhoGA4uj_400x400.jpg"
/>
<v-card-title-stub
class="hero-title"
primarytitle="true"
>
<div>
<h3
class="title"
>
Evan You
</h3>
<div
class="hero-icons"
>
<a
href="https://github.com/yyx990803"
target="_blank"
>
<i
class="fab fa-github"
/>
</a>
<a
href="https://twitter.com/youyuxi"
target="_blank"
>
<i
class="fab fa-twitter"
/>
</a>
</div>
</div>
</v-card-title-stub>
<v-card-actions-stub>
<v-spacer-stub />
<v-btn-stub
activeclass="v-btn--active"
icon="true"
ripple="true"
tag="button"
type="button"
>
<v-icon-stub>
delete
</v-icon-stub>
</v-btn-stub>
</v-card-actions-stub>
</v-card-stub>
</v-flex-stub>
</v-layout-stub>
Let's move to mutation tests now. We're going to check if $apollo
method mutate
is called in our Vue component method addHero()
. There is no data needed to perform this check, because we don't expect any kind of a result here: we just want to be sure a mutation was called. In a new test case we mock $apollo
as shown in the comment above, call addHero()
method and then expect mutate
to be called:
test('called Apollo mutation in addHero() method', () => {
const mutate = jest.fn();
const wrapper = mount(App, {
localVue,
mocks: {
$apollo: {
mutate,
},
},
});
wrapper.vm.addHero();
expect(mutate).toBeCalled();
});
Now we have simple tests coverage for GraphQL query and mutation.
Mocking GraphQL schema
I really wanted to see how my queries are called in a more 'real-life' environment and I've found the solution in this chapter of Apollo docs. The idea is to mock the actual GraphQL schema and call queries and mutations against it.
This part is a bit more complicated and requires more work but from my point of view this way of testing GraphQL calls give you more precise results. Let's start with creating a new mockSchema.js
file in tests
folder and importing required method from graphql-tools
:
import { makeExecutableSchema } from 'graphql-tools';
To create a schema I simply copied a part with all types from apollo-server/schema.graphql
:
const schema = `
type VueHero {
id: ID!
name: String!
image: String
github: String
twitter: String
}
input HeroInput {
name: String!
image: String
github: String
twitter: String
}
type Query {
allHeroes: [VueHero]
}
type Mutation {
addHero(hero: HeroInput!): VueHero!
deleteHero(name: String!): Boolean
}
`;
Now we can create executable schema with imported makeExecutableSchema
method. We should pass our schema as typeDefs
parameter:
export default makeExecutableSchema({
typeDefs: schema,
});
One more thing we need for testing is adding mock functions to schema. Let's do it in our App.spec.js
file:
import { addMockFunctionsToSchema } from 'graphql-tools';
import schema from '../mockSchema';
...
describe('App', () => {
let localVue;
beforeEach(() => {
localVue = createLocalVue();
localVue.use(Vuetify, {});
addMockFunctionsToSchema({
schema,
});
});
...
}):
Now we're ready to test the query.
Testing query with a mocked schema
Let's create a new test case and add a query string to it (you can always check your schema if you're not sure what format should query have):
const query = `
query {
allHeroes {
id
name
twitter
github
image
}
}
`;
Please notice we don't use gql
template literal tag from Apollo here because we will do GraphQL call without including Apollo. We also will set component data after resolving a promise:
graphql(schema, query).then(result => {
wrapper.setData(result.data);
expect(wrapper.element).toMatchSnapshot();
});
At this moment you can delete a previous snapshot and comment/delete a previous query test case
The whole test case should look like this:
test('called allHeroes query with mocked schema', () => {
const query = `
query {
allHeroes {
id
name
twitter
github
image
}
}
`;
const wrapper = shallowMount(App, { localVue });
graphql(schema, query).then(result => {
wrapper.setData(result.data);
expect(wrapper.element).toMatchSnapshot();
});
});
After running it if you check the snapshot file, you might realise all response fields are equal to 'Hello World'. Why does it happen?
The issue is without mocking GraphQL resolvers we will always have a generic response (number of entries will always be 2, all integers will be negative and all strings are Hello World
). But this generic test is good enough to check response structure.
GraphQL resolvers are a set of application specific functions that interact with your underlying datastores according to the query and mutation operations described in your schema.
If you check apollo-server/resolvers
file, you can see that real resolvers are working with data in our database. But test environment doesn't know anything about database, so we need to mock resolvers as well.
Realistic mocking
Let's create mockResolvers.js
file in our test
folder. First thing to add there is a resolver for allHeroes
query:
export default {
Query: {
allHeroes: () => [
{
id: '-pBE1JAyz',
name: 'Evan You',
image:
'https://pbs.twimg.com/profile_images/888432310504370176/mhoGA4uj_400x400.jpg',
twitter: 'youyuxi',
github: 'yyx990803',
},
],
},
};
Now this query will always return the same array with a single entry. Let's add resolvers to the schema in mockSchema.js
:
import resolvers from './mockResolvers';
...
export default makeExecutableSchema({
typeDefs: schema,
resolvers,
});
We also need to change addMockFunctionsToSchema
call in out test suite: in order to keep resolvers not overwritten with mock data, we need to set preserveResolvers
property to true
addMockFunctionsToSchema({
schema,
preserveResolvers: true,
});
Delete previous snapshot and try to run a test. Now we can see a realistic data provided with our resolver in a new snapshot.
We can also add other expectation, because right now we know an exact response structure. Say, we can check if allHeroes
array length is equal 1.
Final version of this test case:
test('called allHeroes query with mocked schema', () => {
const query = `
query {
allHeroes {
id
name
twitter
github
image
}
}
`;
const wrapper = shallowMount(App, { localVue });
graphql(schema, query).then(result => {
wrapper.setData(result.data);
expect(result.data.allHeroes.length).toEqual(1);
expect(wrapper.element).toMatchSnapshot();
});
});
Testing mutation with mocked schema
Now let's test a mutation with our mocked schema too. In the new test case create a mutation string constant:
test('called Apollo mutation in addHero() method', () => {
const mutation = `
mutation {
addHero(hero: {
name: "TestName",
twitter: "TestTwitter",
github: "TestGithub",
image: "TestImage",
}) {
id
name
twitter
github
image
}
}
`;
});
We will pass custom strings as parameters and await for the response. To define this response, let's add a mutation resolver to our mockResolvers
file:
Mutation: {
addHero: (_, { hero }) => ({
id: 1,
name: hero.name,
image: hero.image || '',
twitter: hero.twitter || '',
github: hero.github || '',
}),
},
So our addHero
mutation will return exactly the same hero we passed as its parameter with an id
equal to 1
.
Now we can add a GraphQL query to the test case:
graphql(schema, mutation).then(result => {
expect(result.data.addHero).toBeDefined();
expect(result.data.addHero.name).toEqual('TestName');
});
We didn't check changes to the Vue component instance here but feel free to modify component data with a response.
Full mutation test case:
test('called addHero mutation with mocked schema', () => {
const mutation = `
mutation {
addHero(hero: {
name: "TestName",
twitter: "TestTwitter",
github: "TestGithub",
image: "TestImage",
}) {
id
name
twitter
github
image
}
}
`;
graphql(schema, mutation).then(result => {
expect(result.data.addHero).toBeDefined();
expect(result.data.addHero.name).toEqual('TestName');
});
});
Now our test suit has a basic test for mutate
call and two 'advanced' tests with a mocked GraphQL schema.
If you want to check the project version with all tests, there is a testing
branch here.
Top comments (11)
That’s a great article, Natalia, thank you.
Did you ever have to test Vue components with
<apollo-query>
components in it though?How would you encapsulate the data if the query is not in the
apollo
property?To be honest (and it's completely my personal preference) I prefer not to use Vue Apollo components. I know it's a valid way of doing things and it's a default for e.g. React; but I like my query logic separated from the template.
Perhaps I should try to test components with
<ApolloQuery>
and update an article though, thank you for the great question.Yeah, I am starting to think that the benefits do not compensate the issues with both testing and with Storybook.
I am on the verge of ditching them altogether. I find it also frustrating the lack of documentation about it; while at the same time that the vue-apollo docs express the wonders of using these components, they fail to mention how to test them.
Recently there was a recommendation to use
<apollo-query>
components with inline gql-formatted query, as "best practice". But it stopped there, and any component written this way seems to be untestable with current tools.Hi Daniel, I have the same problem, do you have any progress on this?
No, I just ditched the components alltogether.
I am also thinking of moving on from vue-apollo, especially when Vue3 comes around. Maybe try Villus?
I haven't tried it yet but it looks like an interesting option.
I was able to test the component is rendered and receive the correct properties, what I can't achieve is mock the data that the component returns.
My tests look like this so far:
Hello.vue
Hello.spec.js
Hi Natalia, If you do it would be very helpful and should also be in the documentation
You have a great series of blog posts.
One thing we have struggled to test is dynamic queries or variables in smart queries.
I would love to write a test that is something like
It seems like because
vue-apollo
isn't mounted in test we cant test the variables function directly or indirectly as it doesn't exist on thevm
. Is there a way to get at the apollo section of thevm
in test?Cheers!
You can try to play with mocking
$apollo
object on mounting. I've added this section to Vue Apollo docs recently: vue-apollo.netlify.com/guide/testi...Please let me know if it's useful for your case! If it's not, we'll try to figure out the best way to do it
I decided to test the variables functions directly.
the winning incantation is
I'm working a mock provider that provides access to stuff thing this so it's a bit less of a dig to test
Thanks!
I posted a short blog post for people about this
dev.to/focusedlabs/testing-apollos...