Typescript and Jest: Avoiding type errors on mocked functions - node.js

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?

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.

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

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

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

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

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

Related

how to mock react-query useQuery in jest

I'm trying to mock out axios that is inside an async function that is being wrapped in useQuery:
import { useQuery, QueryKey } from 'react-query'
export const fetchWithAxios = async () => {
...
...
...
const response = await someAxiosCall()
...
return data
}
export const useFetchWithQuery = () => useQuery(key, fetchWithAxios, {
refetchInterval: false,
refetchOnReconnect: true,
refetchOnWindowFocus: true,
retry: 1,
})
and I want to use moxios
moxios.stubRequest('/some-url', {
status: 200,
response: fakeInputData,
})
useFetchWithQuery()
moxios.wait(function () {
done()
})
but I'm getting all sorts of issues with missing context, store, etc which I'm iterested in mocking out completely.
Don't mock useQuery, mock Axios!
The pattern you should follow in order to test your usages of useQuery should look something like this:
const fetchWithAxios = (axios, ...parameters) => {
const data = axios.someAxiosCall(parameters);
return data;
}
export const useFetchWithQuery = (...parameters) => {
const axios = useAxios();
return useQuery(key, fetchWithAxios(axios, ...parameters), {
// options
})
}
Where does useAxios come from? You need to write a context to pass an axios instance through the application.
This will allow your tests to look something like this in the end:
const { result, waitFor, waitForNextUpdate } = renderHook(() => useFetchWithQuery(..., {
wrapper: makeWrapper(withQueryClient, withAxios(mockedAxios)),
});
await waitFor(() => expect(result.current.isFetching).toBeFalsy());

How to mock prisma with jest-mock

I use prisma to interact with my database and i would like to use jest-mock to mock the findMany call. https://jestjs.io/docs/jest-object#jestmockedtitem-t-deep--false
brands.test.ts
import { PrismaService } from "#services/mysql.service";
import { mocked } from "jest-mock";
import faker from "#faker-js/faker";
import { GetBrands } from "./brand";
jest.mock("#services/mysql.service");
/**
* #group unit
*/
describe("Brand", () => {
afterAll(async () => {});
const mockedPrismaService = mocked(PrismaService, true);
it("should get a list of brands", async () => {
const mockedData = [
{
id: faker.datatype.uuid(),
name: faker.datatype.string(),
image: {
source: "some_source",
dtype: "some_dtype",
},
},
];
//#ts-ignore - because of relational data mockedData.image
mockedPrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
const [response, error] = await GetBrands();
console.log(response, error);
});
});
mysql.service.ts
import mysql from "mysql2/promise";
import { Config } from "#config";
import { PrismaClient, Prisma } from "#prisma/client";
export const MySQLEscape = mysql.escape;
export const MySQLPreparedStatement = mysql.format;
export const PrismaService = new PrismaClient({});
export const PrismaHelper = Prisma;
However when i run this test i get the following error.
TypeError: Cannot read properties of undefined (reading 'brand')
Factory Mock
One option is to option use the factory approach when mocking your client.
jest.mock("#services/mysql.service", () => ({
PrismaService: {
brand: {
findMany: jest.fn(() => { })
}
},
}));
Then within your test, you can mock the findMany function to return your test data, then call the function being tested.
const mockedData = [...];
PrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
const result = await GetBrands();
It's a bit cumbersome, but it works.
Note that in my example, I've implemented GetBrands as follows:
import { PrismaService } from "#services/mysql.service"
export const GetBrands = async () => {
const data = await PrismaService.brand.findMany();
return data;
}
Your example
In your example, you're using automatic mocking, and I'm not too familiar with it so I'm not sure how to get it working.
What seems to be happening to cause the error is your PrismaService is undefined when it's imported here:
import { PrismaService } from "#services/mysql.service";
And then calling the mocked function with an undefined parameter returns undefined:
const mockedPrismaService = mocked(undefined, true); // returns undefined
And finally, calling the following is what throws the error:
mockedPrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
// TypeError: Cannot read properties of undefined (reading 'brand')
I would have thought something like this would be what you're after, but this throws an error:
jest.mock("#services/mysql.service", () => ({
PrismaService: mocked(PrismaService, true)
}));
// 6 |
// 7 | jest.mock("#services/mysql.service", () => ({
//> 8 | PrismaService: mocked(PrismaClient, true)
// | ^
// 9 | }));
Check out the docs
Might be worth checking out the Prismas documentation on unit testing, as they suggest a couple of pretty different approaches.

Import module with folder and passing data to module in nodejs

I Found The Tutorial about
Designing a clean REST API with Node.js (Express + Mongo)
project in github.
but the problem is i didn't get the concept of routing in one part.
the misundrestanding part is how is it possible to pass httpRequest data to handle method within contact-endpoint module?
because handle method is in here export default function makeContactsEndpointHandler({ contactList }) {
return async function handle(httpRequest) {
this is the index of project:
import handleContactsRequest from "./contacts";
import adaptRequest from "./helpers/adapt-request";
app.all("/contacts", contactsController);
app.get("/contacts/:id", contactsController);
function contactsController(req, res) {
const httpRequest = adaptRequest(req);
handleContactsRequest(httpRequest)
.then(({ headers, statusCode, data }) =>
res.set(headers).status(statusCode).send(data)
)
.catch((e) => res.status(500).end());
}
this is the adaptRequest:
export default function adaptRequest (req = {}) {
return Object.freeze({
path: req.path,
method: req.method,
pathParams: req.params,
queryParams: req.query,
body: req.body
})
}
this is the handleContactsRequest module:
import makeDb from "../db";
import makeContactList from "./contact-list";
import makeContactsEndpointHandler from "./contacts-endpoint";
const database = makeDb();
const contactList = makeContactList({ database });
const contactsEndpointHandler = makeContactsEndpointHandler({ contactList });
export default contactsEndpointHandler;
this is part of contact-endpoint module:
export default function makeContactsEndpointHandler({ contactList }) {
return async function handle(httpRequest) {
switch (httpRequest.method) {
case "POST":
return postContact(httpRequest);
case "GET":
return getContacts(httpRequest);
default:
return makeHttpError({
statusCode: 405,
errorMessage: `${httpRequest.method} method not allowed.`,
});
}
}
makeContactsEndpointHandler is a function that returns a function (async handle(xxx)).
In handleContactsRequest, we export the result of the call: makeContactsEndpointHandler({ contactList }). Which is therefore the function async handle(xxx) itself.
So, in index, when we call handleContactsRequest with the constant httpRequest as argument, we're actually calling that handle(xxx) function. (I wrote xxx as parameter name to highlight the difference between the two httpRequest declarations.)

How to mock the api call in jest for saga test

I am writing tests to test my saga. Can anyone guide me how I can change the code below so that I can mock the api call? I don`t want to test real data.
import { call, put } from 'redux-saga/effects';
import { API_BUTTON_CLICK_SUCCESS, } from './actions/consts';
import { getDataFromAPI } from './api';
it('apiSideEffect - fetches data from API and dispatches a success action', () => {
const generator = apiSideEffect();
expect(generator.next().value)
.toEqual(call(getDataFromAPI));
expect(generator.next().value)
.toEqual(put({ type: API_BUTTON_CLICK_SUCCESS }));
expect(generator.next())
.toEqual({ done: true, value: undefined });
});
The getDataFromAPI()
import axios from "axios";
export const getDataFromAPI =(
method: string,
url: string,
path: string,
data?: any
) =>{
switch (method) {
case "create": {
return axios
.post(url + path, data, {
headers: {
Accept: "application/json",
"content-type": "application/json"
}
})
.catch(error => {
throw error.response;
});
}
I have tried to use
jest.mock('../../src/Utilities/api');
const { callApi } = require('../../src/Utilities/api');
callApi.mockImplementation( () => console.log("some api call"));
I am having the error
TypeError: Cannot read property 'mockImplementation' of undefined
at Object.<anonymous> (src/Payments/PaymentSagas.spec.ts:10:17)
at new Promise (<anonymous>)
at Promise.resolve.then.el (node_modules/p-map/index.js:46:16)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
I usually do
import * as apis from '../../src/Utilities/api';
jest.spyOn(api, "callApi");
api.callApi.mockImplementation(/* your mock */);
easily exportable as a per-se function
export function spyUtil(obj, name, mockFunction = undefined) {
const spy = jest.spyOn(obj, name);
let mock;
if (mockFunction) {
mock = jest.fn(mockFunction);
obj[name].mockImplementation(mock);
}
return { spy, mock };
}
and consumable, in your test
spyUtil(apis, "callApi", jest.fn())

Jest unit test to spy on lower-level method (NodeJS)

Trying to spy and override a function two levels down using Jest.
The test results say, "Expected mock function to have been called, but it was not called."
// mail/index.unit.test.js
import mail from './index';
import * as sib from '../sendinblue';
describe('EMAIL Util', () =>
test('should call sibSubmit in server/utils/sendinblue/index.js', async() => {
const sibMock = jest.spyOn(sib, 'sibSubmit');
sibMock.mockImplementation(() => 'Calling sibSubmit()');
const testMessage = {
sender: [{ email: 'foo#example.com', name: 'Something' }],
to: [{ email: 'foo#example.com', name: 'Something' }],
subject: 'My Subject',
htmlContent: 'This is test content'
};
await mail.send(testMessage);
expect(sibMock).toHaveBeenCalled();
})
);
mail.send() comes from here...
// mail/index.js
import { sibSendTransactionalEmail } from '../sendinblue';
export default {
send: async message => {
try {
return await sibSendTransactionalEmail(message);
} catch(err) {
console.error(err);
}
}
};
Which uses SendInBlue's API via axios (why I need to mock)...
// sendinblue/index.js
import axios from 'axios';
import config from '../../config/environment';
export async function sibSubmit(method, url, data) {
let instance = axios.create({
baseURL: 'https://api.sendinblue.com',
headers: { 'api-key': config.mail.apiKey }
});
try {
const response = await instance({
method,
url,
data
});
return response;
} catch(err) {
console.error('Error communicating with SendInBlue', instance, err);
}
}
export const sibSendTransactionalEmail = message => sibSubmit('POST', '/v3/smtp/email', message);
I assumed mail.send() would call sibSendTransactionalEmail() in the other module and it would call sibSubmit(), the focus of jest.spyOn(). Wondering where I went wrong.
jest.spyOn replaces the method on the object it is passed with a spy.
In this case you are passing sib which represents the ES6 module exports from sendinblue.js, so Jest will replace the module export for sibSubmit with the spy and give the spy the mock implementation you provided.
mail.send then calls sibSendTransactionalEmail which then calls sibSubmit directly.
In other words, your spy is not called because sibSendTransactionalEmail does not call the module export for sibSubmit, it is just calling sibSubmit directly.
An easy way to resolve this is to note that "ES6 modules support cyclic dependencies automatically" so you can simply import the module into itself and call sibSubmit from within sibSendTransactionalEmail using the module export:
import axios from 'axios';
import config from '../../config/environment';
import * as sib from './'; // import module into itself
export async function sibSubmit(method, url, data) {
let instance = axios.create({
baseURL: 'https://api.sendinblue.com',
headers: { 'api-key': config.mail.apiKey }
});
try {
const response = await instance({
method,
url,
data
});
return response;
} catch(err) {
console.error('Error communicating with SendInBlue', instance, err);
}
}
export const sibSendTransactionalEmail = message => sib.sibSubmit('POST', '/v3/smtp/email', message); // call sibSubmit using the module export
Note that replacing ES6 module exports with jest.spyOn like this works because Jest transpiles the ES6 modules to Node modules in a way that allows them to be mutated
Another way to work around this problem is to rewire the function you're spying on within the module, which is nicer since you don't have to modify the original code for the purposes of testing. You can use the rewire module if before ES6, or babel-rewire for ES6:
// mail/index.unit.test.js
import mail from './index';
import * as sib from '../sendinblue';
describe('EMAIL Util', () =>
test('should call sibSubmit in server/utils/sendinblue/index.js', async() => {
const sibMock = jest.spyOn(sib, 'sibSubmit');
sibMock.mockImplementation(() => 'Calling sibSubmit()');
//============ force the internal calls to use the mock also
sib.__set__("sibSubmit", sibMock);
//============
const testMessage = {
sender: [{ email: 'foo#example.com', name: 'Something' }],
to: [{ email: 'foo#example.com', name: 'Something' }],
subject: 'My Subject',
htmlContent: 'This is test content'
};
await mail.send(testMessage);
expect(sibMock).toHaveBeenCalled();
})
);

Resources