Mock Fetch in TypeScript Jest Tests for Vue.js

May 17th 2019 Unit Testing Jest Vue.js

When creating my first Vue.js project I configured it for TypeScript support and Jest based unit testing. This might not be the most common setup but it does express my tooling preferences. To no surprise, the first component I wanted to unit test used fetch which meant I also had to use jest-fetch-mock to mock its calls. There were a couple of obstacles on the way to the first working test.

Configuring jest-fetch-mock

Fortunately, jest-fetch-mock documentation includes a short setup guide for TypeScript users. Although the instructions work perfectly for a blank project with ts-jest based testing configuration, they didn't work in the project I created using Vue CLI. The tests failed to run with the following error:

Test suite failed to run

TypeScript diagnostics (customize using [jest-config].globals.ts-jest.diagnostics option):

setupJest.ts:3:43 - error TS2304: Cannot find name 'global'.

3 const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;

This wasn't the first time I encountered problems with missing type declarations in TypeScript. As it turned out, they were again caused by the types compiler option in tsconfig.ts as generated by Vue CLI:

"types": [
    "webpack-env",
    "jest"
],

This overrides automatic inclusion of all visible types from node_modules in the compilation. Here are some key quotes from the documentation:

By default all visible @types packages are included in your compilation.

If typeRoots is specified, only packages under typeRoots will be included.

If types is specified, only packages listed will be included.

The solution? I added node to the list of types:

"types": [
    "node",
    "webpack-env",
    "jest"
],

The Vue.js sample tests worked again. However, as soon as I tried using fetchMock in my test files, Visual Studio Code complained about it:

Cannot find name 'fetchMock'.

To get rid of the error, I added just-fetch-mock to the types compiler option as well:

"types": [
    "node",
    "webpack-env",
    "jest",
    "jest-fetch-mock"
],

I was now ready to start writing tests.

Strongly-Typed Single-File Vue Components

I'm writing Vue components using the class-style syntax and putting them in single files. For most cases this works just fine. However, the type information about component members gets lost when they're imported into a different file. When writing unit tests, this is a standard practice:

import HelloWorld from '@/components/HelloWorld.vue';
import { mount } from "@vue/test-utils";

it('succeeds', done => {
    fetchMock.mockResponseOnce('', { status: 200 });

    const wrapper = mount(HelloWorld);
    wrapper.vm.validate().then(success => {
        expect(success).toBe(true);
        done();
    });
});

The above code fails with the following error:

Property 'validate' does not exist on type 'CombinedVueInstance<Vue, object, object, object, Record>'.

And yes, there is a validate method in the HelloWorld component:

import { Component, Prop, Vue } from "vue-property-decorator";

@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;

  public validate(): Promise<boolean> {
    return fetch('/api/validate/').then(response => {
      return response.ok;
    });
  }
}

Thanks to Visual Studio Code I quickly determined that default imports from .vue files are always typed as Vue. The explanation for that can be found in shims-vue.d.ts:

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

Without this piece of code, importing from .vue files wouldn't work at all.

If I wanted some type safety in my unit tests, I had to find a workaround. I decided to create a companion interface for every component and put it in a separate .ts file:

export interface HelloWorldComponent {
    validate: () => Promise<boolean>;
}

To make sure that the type information is correct, the component implements that interface:

import { Component, Prop, Vue } from "vue-property-decorator";
import { HelloWorldComponent } from "./HelloWorld";

@Component
export default class HelloWorld extends Vue implements HelloWorldComponent {
  @Prop() private msg!: string;

  public validate(): Promise<boolean> {
    return fetch('/api/validate/').then(response => {
      return response.ok;
    });
  }
}

In tests, I can now cast the component to the interface and then access the members with full type information:

import HelloWorld from '@/components/HelloWorld.vue';
import { HelloWorldComponent } from '@/components/HelloWorld';
import { mount } from "@vue/test-utils";

it('succeeds', done => {
    fetchMock.mockResponseOnce('', { status: 200 });

    const wrapper = mount(HelloWorld);
    const vm: HelloWorldComponent = wrapper.vm as any;
    vm.validate().then(success => {
        expect(success).toBe(true);
        done();
    });
});

To avoid casting in every test, I wrote a simple helper function which takes care of that:

import { VueClass, Wrapper, mount, ThisTypedMountOptions } from '@vue/test-utils';
import { Vue } from "vue-property-decorator";

export function createWrapper<V extends Vue, T>(
    component: VueClass<V>,
    options?: ThisTypedMountOptions<V>
): Wrapper<V & T> {
    return mount(component, options) as any;
}

I call this function instead of mount directly:

import HelloWorld from '@/components/HelloWorld.vue';
import { HelloWorldComponent } from '@/components/HelloWorld';
import { createWrapper } from './TestHelpers';

it('succeeds', done => {
    fetchMock.mockResponseOnce('', { status: 400 });

    const wrapper = createWrapper<HelloWorld, HelloWorldComponent>(HelloWorld);
    wrapper.vm.validate().then(success => {
        expect(success).toBe(false);
        done();
    });
});

To further simplify initialization in individual tests, I extended the helper function with mocking configuration for common plugins that many components depend on.

Although there's some overhead to the approach I've taken, I'm quite satisfied with it. Especially considering that the current version of Vue.js wasn't developed with TypeScript in mind. Vue 3.x is promised to bring further improvements in this field. I'm already looking forward to it.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License