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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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

Akryum avatar
Akryum commented on

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()
Enter fullscreen mode Exit fullscreen mode
View on GitHub

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 });
  });
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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',
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
  });
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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
  } 
`;
Enter fullscreen mode Exit fullscreen mode

Now we can create executable schema with imported makeExecutableSchema method. We should pass our schema as typeDefs parameter:

export default makeExecutableSchema({
  typeDefs: schema,
});
Enter fullscreen mode Exit fullscreen mode

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,
    });
  });
  ...
}):
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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();
    });
});
Enter fullscreen mode Exit fullscreen mode

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',
      },
    ],
  },
};
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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();
    });
});
Enter fullscreen mode Exit fullscreen mode

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
          }
        }
    `;
});
Enter fullscreen mode Exit fullscreen mode

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 || '',
    }),
},
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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');
    });
});
Enter fullscreen mode Exit fullscreen mode

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.

Logo

前往低代码交流专区

更多推荐