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.
所有评论(0)