DEV Community

loading...
Cover image for Testing vue-apollo Components with Jest

Testing vue-apollo Components with Jest

n_tepluhina profile image Natalia Tepluhina Updated on ・7 min read

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

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.

Discussion (11)

pic
Editor guide
Collapse
danroc profile image
Daniel da Rocha

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?

Collapse
n_tepluhina profile image
Natalia Tepluhina Author

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.

Collapse
danroc profile image
Daniel da Rocha

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.

Thread Thread
tavo1987 profile image
Edwin Ramírez • Edited

Hi Daniel, I have the same problem, do you have any progress on this?

Thread Thread
danroc profile image
Daniel da Rocha

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?

Thread Thread
tavo1987 profile image
Edwin Ramírez • Edited

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

<template>
    <ApolloQuery
            v-test="{ id: 'apollo-query' }"
            :query="require('@/graphql/MY_QUERY').default"
            :variables="{ id: 1 }"
            :notify-on-network-status-change="true"
          >
      <template v-slot="{ result: { loading, error, data } }">
        <div v-if="loading">
          Loading...
        </div>
        <div v-else-if="error">An error occurred</div>
        <div v-else-if="data">{{ data.hello }}</div>
        <div v-else>No result :(</div>
      </template>
    </ApolloQuery>
</template>

Hello.spec.js

import { mount, createLocalVue } from '@vue/test-utils'
import VueApollo from 'vue-apollo'
import Hello from '@/components/Hello'

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


describe('Hello.vue', () =>() {
    test('renders an ApolloQuery component', () =>{
        const wrapper = moun(Hello, {
            localvue,
            // To remove error in render: "TypeError: Cannot read property 'loading' of undefined"
            // More info about this https://github.com/vuejs/vue-apollo/issues/609 
            $apolloData: {
              loading: false
            },
        })

        // I'm using a custom directive v-test that transform this v-test="{id: 'foo'}" to [data-test-id=foo]
        const apolloQuery = wrapper.find('[data-test-id=apollo-query]')

        expect(apolloQuery.exists()).toBe(true)
        expect(apolloQuery.props().query).toBe(require('@/graphql/MY_QUERY').default)
        expect(apolloQuery.props().variables).toEqual({ id: 1 })
        expect(apolloQuery.props().notifyOnNetworkStatusChange).toBeTruthy()
    })
})
Collapse
tavo1987 profile image
Edwin Ramírez

Hi Natalia, If you do it would be very helpful and should also be in the documentation

Collapse
austinbv profile image
Austin Vance

You have a great series of blog posts.

One thing we have struggled to test is dynamic queries or variables in smart queries.

Vue.extend({
  name: 'TestComponent',
  apollo: {
    human: {
      query: gql`query GetHumans($ID: ID!) {
      human(id: $id) {
        name
        height
      }
    }`,
    },
    variables() {
      return {
        id: this.$route.params.id,
      };
    },
  },
});

I would love to write a test that is something like


it('gets queries for the correct user', () => {
  mount(TestComponent, {
    mocks: {
      params: {
        id: "1"
      }
    }
  })

  // expect query to be called with an ID of "1"
})

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 the vm. Is there a way to get at the apollo section of the vm in test?

Cheers!

Collapse
n_tepluhina profile image
Natalia Tepluhina Author

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

Collapse
austinbv profile image
Austin Vance • Edited

I decided to test the variables functions directly.

the winning incantation is

const wrapper = mountVue(GroupsIndex, {
  mocks: {
    $route: {
      params: { userId: 6 },
      query: { zip: 50500 },
    },
  },
});

// @ts-ignore
expect(wrapper
  .vm
  .$options
  .apollo
  .groups
  .variables
  .bind(wrapper.vm)().zip
).toEqual(50500);

// @ts-ignore
expect(
  wrapper.
  vm.
  $options.
  apollo.
  groups.
  variables.
  bind(wrapper.vm)().userId
).toEqual(6);

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!

Thread Thread
austinbv profile image
Austin Vance

I posted a short blog post for people about this

dev.to/focusedlabs/testing-apollos...