类型脚本和 Jest: 避免模拟函数上的类型错误

当希望使用 Jest 模拟外部模块时,我们可以使用 jest.mock()方法自动模拟模块上的函数。

然后,我们可以随心所欲地操作和询问模拟模块上的模拟函数。

例如,考虑下面这个模仿 axios 模块的人为例子:

import myModuleThatCallsAxios from '../myModule';
import axios from 'axios';


jest.mock('axios');


it('Calls the GET method as expected', async () => {
const expectedResult: string = 'result';


axios.get.mockReturnValueOnce({ data: expectedResult });
const result = await myModuleThatCallsAxios.makeGetRequest();


expect(axios.get).toHaveBeenCalled();
expect(result).toBe(expectedResult);
});

上面的代码在 Jest 中运行良好,但会抛出一个类型错误:

类型不存在的属性’仿回值一次’(url: 字符串,配置? : AxiosRequestConfig | 未定义) = > Axios 。

axios.get的 typedef 正确地不包括 mockReturnValueOnce属性。我们可以通过将其包装为 Object(axios.get)来强制 Typecript 将 axios.get作为 Object 文本处理,但是:

在维护类型安全的同时模拟函数的惯用方法是什么?

98539 次浏览

A usual approach to provide new functionality to imports to extend original module like declare module "axios" { ... }. It's not the best choice here because this should be done for entire module, while mocks may be available in one test and be unavailable in another.

In this case a type-safe approach is to assert types where needed:

  (axios.get as jest.Mock).mockReturnValueOnce({ data: expectedResult });
...
expect(axios.get as jest.Mock).toHaveBeenCalled();

To idiomatically mock the function while maintaining type safety use spyOn in combination with mockReturnValueOnce:

import myModuleThatCallsAxios from '../myModule';
import axios from 'axios';


it('Calls the GET method as expected', async () => {
const expectedResult: string = 'result';


// set up mock for axios.get
const mock = jest.spyOn(axios, 'get');
mock.mockReturnValueOnce({ data: expectedResult });


const result = await myModuleThatCallsAxios.makeGetRequest();


expect(mock).toHaveBeenCalled();
expect(result).toBe(expectedResult);


// restore axios.get
mock.mockRestore();
});

Add this line of code const mockedAxios = axios as jest.Mocked<typeof axios>. And then use the mockedAxios to call the mockReturnValueOnce. With your code, should be done like this:

import myModuleThatCallsAxios from '../myModule';
import axios from 'axios';


jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;


it('Calls the GET method as expected', async () => {
const expectedResult: string = 'result';


mockedAxios.get.mockReturnValueOnce({ data: expectedResult });
const result = await myModuleThatCallsAxios.makeGetRequest();


expect(mockedAxios.get).toHaveBeenCalled();
expect(result).toBe(expectedResult);
});

Please use the mocked function from ts-jest

The mocked test helper provides typings on your mocked modules and even their deep methods, based on the typing of its source. It makes use of the latest TypeScript feature, so you even have argument types completion in the IDE (as opposed to jest.MockInstance).

import myModuleThatCallsAxios from '../myModule';
import axios from 'axios';
import { mocked } from 'ts-jest/utils'


jest.mock('axios');


// OPTION - 1
const mockedAxios = mocked(axios, true)
// your original `it` block
it('Calls the GET method as expected', async () => {
const expectedResult: string = 'result';


mockedAxios.mockReturnValueOnce({ data: expectedResult });
const result = await myModuleThatCallsAxios.makeGetRequest();


expect(mockedAxios.get).toHaveBeenCalled();
expect(result).toBe(expectedResult);
});


// OPTION - 2
// wrap axios in mocked at the place you use
it('Calls the GET method as expected', async () => {
const expectedResult: string = 'result';


mocked(axios).get.mockReturnValueOnce({ data: expectedResult });
const result = await myModuleThatCallsAxios.makeGetRequest();


// notice how axios is wrapped in `mocked` call
expect(mocked(axios).get).toHaveBeenCalled();
expect(result).toBe(expectedResult);
});

I can't emphasise how great mocked is, no more type-casting ever.

@hutabalian The code works really well when you use axios.get or axios.post but if you use a config for requests the following code:

const expectedResult: string = 'result';
const mockedAxios = axios as jest.Mocked<typeof axios>;
mockedAxios.mockReturnValueOnce({ data: expectedResult });

Will result in this error:

TS2339 (TS) Property 'mockReturnValueOnce' does not exist on type 'Mocked'.

You can solve it like this instead:

AxiosRequest.test.tsx

import axios from 'axios';
import { MediaByIdentifier } from '../api/mediaController';


jest.mock('axios', () => jest.fn());


test('Test AxiosRequest',async () => {
const mRes = { status: 200, data: 'fake data' };
(axios as unknown as jest.Mock).mockResolvedValueOnce(mRes);
const mock = await MediaByIdentifier('Test');
expect(mock).toEqual(mRes);
expect(axios).toHaveBeenCalledTimes(1);
});

mediaController.ts:

import { sendRequest } from './request'
import { AxiosPromise } from 'axios'
import { MediaDto } from './../model/typegen/mediaDto';


const path = '/api/media/'


export const MediaByIdentifier = (identifier: string): AxiosPromise<MediaDto> => {
return sendRequest(path + 'MediaByIdentifier?identifier=' + identifier, 'get');
}

request.ts:

import axios, { AxiosPromise, AxiosRequestConfig, Method } from 'axios';


const getConfig = (url: string, method: Method, params?: any, data?: any) => {
const config: AxiosRequestConfig = {
url: url,
method: method,
responseType: 'json',
params: params,
data: data,
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' },
}
return config;
}


export const sendRequest = (url: string, method: Method, params?: any, data?: any): AxiosPromise<any> => {
return axios(getConfig(url, method, params, data))
}

After updating to the newest Axios (0.21.1) I started to have this kind of problem. I tried a lot of solutions but with no result.

My workaround:

type axiosTestResponse = (T: unknown) => Promise<typeof T>;


...


it('some example', async () => {
const axiosObject = {
data: { items: [] },
status: 200,
statusText: 'ok',
headers: '',
config: {},
} as AxiosResponse;


(Axios.get as axiosTestResponse) = () => Promise.resolve(axiosObject);
});

Starting with ts-jest 27.0 mocked from ts-jest will be deprecated and removed in 28.0 you can check it in the official documentation. So please use instead mocked from jest. Here's the documentation

mocked from ts-jestwill be deprecated and removed in 28.0

So for your example:

import myModuleThatCallsAxios from '../myModule';
import axios from 'axios';


jest.mock('axios');


// OPTION - 1
const mockedAxios = jest.mocked(axios, true)
// your original `it` block
it('Calls the GET method as expected', async () => {
const expectedResult: string = 'result';


mockedAxios.mockReturnValueOnce({ data: expectedResult });
const result = await myModuleThatCallsAxios.makeGetRequest();


expect(mockedAxios.get).toHaveBeenCalled();
expect(result).toBe(expectedResult);
});