130

When wanting to mock external modules with Jest, we can use the jest.mock() method to auto-mock functions on a module.

We can then manipulate and interrogate the mocked functions on our mocked module as we wish.

For example, consider the following contrived example for mocking the axios module:

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);
});

The above will run fine in Jest but will throw a Typescript error:

Property 'mockReturnValueOnce' does not exist on type '(url: string, config?: AxiosRequestConfig | undefined) => AxiosPromise'.

The typedef for axios.get rightly doesn't include a mockReturnValueOnce property. We can force Typescript to treat axios.get as an Object literal by wrapping it as Object(axios.get), but:

What is the idiomatic way to mock functions while maintaining type safety?

duncanhall
  • 11,035
  • 5
  • 54
  • 86
  • Maybe another approach is to use assignment like `axios.get = jest.fn()` i.e. https://github.com/dvallin/vuejs-tutorial/blob/bde8a229f4e5710b5ec5d45d56b07a77f61f36a3/frontend/test/api/tasks.spec.ts#L7 – yurzui Jul 29 '18 at 06:32

7 Answers7

188

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);
});
hutabalian
  • 3,254
  • 2
  • 16
  • 13
  • 22
    I also tried this method with 'mockedAxios.get.getResolvedValueOnce' and got TypeError: mockedAxios.get.mockResolvedValueOnce is not a function – Safa Alai Apr 04 '20 at 22:10
  • 7
    I tried this method and I got Argument of type '{ data: string; }' is not assignable to parameter of type 'Promise'. Also when I run it I get mockReturnedValue is not a function. – Safa Alai Apr 04 '20 at 22:17
  • I had to use `const mockedFetch = fetch as any` when using fetch – Aidan Grimshaw Sep 01 '20 at 22:49
69

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.

mikemaccana
  • 110,530
  • 99
  • 389
  • 494
Ankeet Maini
  • 732
  • 6
  • 8
  • 7
    I get only an error that `TypeError: ts_jest_1.mocked(...).sendMessage.mockReturnValue is not a function` – Ben Keil Mar 28 '20 at 10:20
  • This is a no brainer if you're using jest-preset-angular as it comes with ts-jest as a dependency – devakone Apr 02 '20 at 22:13
  • 3
    updated link to mocked docs: https://kulshekhar.github.io/ts-jest/docs/guides/test-helpers/ – Gabriel Vilches Alves Apr 24 '21 at 23:50
  • This **is not** a solution. The solution below should be accepted. – Hexworks Nov 22 '21 at 13:59
  • 10
    mocked is now deprecated and will be removed in 28.0.0. The function has been integrated into jest-mock package as a part of Jest 27.4.0, see https://github.com/facebook/jest/pull/12089. Please use the one from jest-mock instead. – Joel Dec 28 '21 at 17:19
39

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();
});
Brian Adams
  • 43,011
  • 9
  • 113
  • 111
  • 20
    missing typescript implementation – Eduard Jacko May 10 '19 at 17:59
  • This expects a parameter of type void when you set the `mockReturnValueOnce(...)` – Big Money Aug 27 '19 at 23:29
  • I tried this method with mockReturnValueOnce and got: Argument of type '{ data: string; }' is not assignable to parameter of type 'Promise'. However, the test runs and succeeds. Then I tried this with mockResolvedValueOnce( () => {data:'hello'} ) and both the compile error and runtime errors were solved. – Safa Alai Apr 04 '20 at 22:15
15

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();
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
10

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 jest.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);
});

Melchia
  • 22,578
  • 22
  • 103
  • 117
6

@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))
}
Ogglas
  • 62,132
  • 37
  • 328
  • 418
0

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);
});
Ridd
  • 10,701
  • 3
  • 19
  • 20