Mock Fetch in TypeScript Jest Tests for 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 undertypeRoots
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.