Vue, Vitest and MSW: Mocking and testing a GraphQL query result in composable not possible

772 views Asked by At

EDIT

I've made a stackblitz example where I have the most barebones of the problem. You can find it here: https://stackblitz.com/edit/vitejs-vite-axxjsu?file=src%2Fcomposables%2FuseFetchCountries.spec.ts

EDIT END

I made a simple dummy application as in the end it will fit in a much bigger enterprise application, but I first made a proof of concept to make sure it works... and it doesn't yet! I'm at a loss why this is.

I've followed the documentation on https://mswjs.io/ for getting the mock in a test runner. So I've got a src/mocks/server.ts file where I'm setting up the server with the handlers. I've got a src/setup-file.ts for Vitest which is stated in the vitest.config.ts setupFiles property to do the beforeAll, afterEach and afterAll stated in the MSW documentation. I see in the console when running the application that requests are mocked as it does intercept it. However, it does nothing in the test runner.

I've got a simple composable that fetches countries from a public graphql endpoint (just for testing :-)).

// /src/composables/useFetchCountries.ts
const GET_COUNTRIES = gql`
  query GetCountries {
    countries {
      code
      name
      currency
    }
  }
`;

const useFetchCountries = () => {
  const { result } = useQuery<GetCountriesQuery>(GET_COUNTRIES);

  const countries = computed(() => result.value?.countries);

  return {
    countries,
  };
};

This is then retrieved in the App.vue.

<template>
  <h1>Some countries</h1>
  <ul>
    <li v-for="country in countries" :key="country.code">
      {{ country.name }} | {{ country.code }}
    </li>
  </ul>
</template>

<script setup lang="ts">
  import useFetchCountries from "./composables/useFetchCountries";
  const { countries } = useFetchCountries();
</script>

Then my test for the composable is as follows (:

// /src/composables/useFetchCountries.spec.ts
import { provideApolloClient } from "@vue/apollo-composable";
import { createMockClient } from "mock-apollo-client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import useFetchCountries from "./useFetchCountries";

let mockApolloClient = createMockClient();

describe.only("composable - useFetchCountries", () => {
  beforeEach(() => {
    mockApolloClient = createMockClient();
    provideApolloClient(mockApolloClient);
  });
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it("retrieves all a couple countries", async () => {
    const { countries } = useFetchCountries();

    const dummy = [
      { code: "AD", name: "Andorra", currency: "EUR" },
      { code: "AT", name: "Austria", currency: "EUR" },
      { code: "AX", name: "Åland", currency: "EUR" },
      { code: "BE", name: "Belgium", currency: "EUR" },
      { code: "BL", name: "Saint Barthélemy", currency: "EUR" },
    ];

    expect(countries.value).toBe(dummy);
  });
});

All right, now finally what I have in the handlers to intercept the GetCountries query and return a mock.

// /src/mocks/handlers.ts
import { HttpResponse, graphql } from "msw";

const coupleCountries = new Map([
  ["AD", { code: "AD", name: "Andorra", currency: "EUR" }],
  ["AT", { code: "AT", name: "Austria", currency: "EUR" }],
  ["AX", { code: "AX", name: "Åland", currency: "EUR" }],
  ["BE", { code: "BE", name: "Belgium", currency: "EUR" }],
  ["BL", { code: "BL", name: "Saint Barthélemy", currency: "EUR" }],
]);

export const handlers = [
  graphql.query("GetCountries", ({ query }) => {
    console.log('Intercepted "GetCountries" query: ', query);
    return HttpResponse.json({
      data: {
        countries: Array.from(coupleCountries.values()),
      },
    });
  }),
];

However because of Vue computes, countries is already undefined upon running the test! I have no clue how to fix this as I only started working with Vue a little while ago. I get the main gist of it, however with writing more advanced tests like this, I'm completely in the dark.

In the bigger application there are certain methods in the composable that do things with the data that is retrieved from the useQuery. Those methods I want to test to ensure they are proper and not changed later on causing side effect. Any ideas how to fix the test above to make sure the mocked data is actually inside the test?

2

There are 2 answers

1
Henry Zarza On BEST ANSWER

I just started to learn VueJS some days ago too, so I don't know if it is or is not a good practice my solution

I found on @vue/test-utils docs that there is a method that you can call named: flushPromises, here is the explanation:

flushPromises flushes all resolved promise handlers. This helps make sure async operations such as promises or DOM updates have happened before asserting against them.

And here is the link to the official docs

I realized you could mock the response without using msw just using mock-apollo-client library, I followed this example they had in the documentation.

So, with that being said, I did this and it worked for me:

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import { createMockClient } from 'mock-apollo-client'
import { provideApolloClient } from '@vue/apollo-composable'

import { useFetchCountries, GET_COUNTRIES } from './useFetchCountries'

const coupleCountries = new Map([
  ["AD", { code: "AD", name: "Andorra", currency: "EUR" }],
  ["AT", { code: "AT", name: "Austria", currency: "EUR" }],
  ["AX", { code: "AX", name: "Åland", currency: "EUR" }],
  ["BE", { code: "BE", name: "Belgium", currency: "EUR" }],
  ["BL", { code: "BL", name: "Saint Barthélemy", currency: "EUR" }],
]);

describe('About View', () => {
  beforeEach(() => {
    const mockClient = createMockClient()

    mockClient.setRequestHandler(
      GET_COUNTRIES,
      () => Promise.resolve({ data: { countries: Array.from(coupleCountries.values()) }}));

    provideApolloClient(mockClient)
  })

  afterEach(() => {
    vi.restoreAllMocks()
  })

  it("retrieves all a couple countries", async () => {
    const { countries } = await useFetchCountries();

    await flushPromises();

    const dummy = [
      { code: "AD", name: "Andorra", currency: "EUR" },
      { code: "AT", name: "Austria", currency: "EUR" },
      { code: "AX", name: "Åland", currency: "EUR" },
      { code: "BE", name: "Belgium", currency: "EUR" },
      { code: "BL", name: "Saint Barthélemy", currency: "EUR" },
    ];

    expect(countries.value).toStrictEqual(dummy);
  });
})

2
paddyfields On

You don't seem to be awaiting the result of useFetchCountries()

I can't test this without spinning up a whole demo, but I'd recommend these changes so that you await the graphQL call in your tests:

  // /src/composables/useFetchCountries.spec.ts
  it("retrieves all a couple countries", async () => {
    const { countries } = await useFetchCountries();
    ...
  });
  // /src/composables/useFetchCountries.ts
  const useFetchCountries = async () => {
    const { result } = await useQuery<GetCountriesQuery>(GET_COUNTRIES);
    ...
  }

And also make your useQuery function async.