Jest mock unnable to pick up error throw? - javascript

I have some code that calls an api. I am testing the functionality that is nested in try catches, and checking the correct behaviour happens when I throw an error with certain values.
async function createDeployment(namespace, config, name) {
try {
await k8sDeploymentApi.createNamespacedDeployment(namespace, config, true);
console.log('Successful deployment');
return Promise.resolve(true);
} catch (e) {
console.log(JSON.stringify(e));
if (e.response.statusCode === HTTP_CONFLICT) {
try {
await k8sDeploymentApi.replaceNamespacedDeployment(name, namespace, config, true);
console.log('Successfully replaced deployment');
return Promise.resolve(true);
} catch (error) {
return Promise.reject(error);
}
}
return Promise.reject(e);
}
}
I am mocking out the api calls node module here
var mockListNamespacedIngress;
var mockCreateNamespacedDeployment;
var mockReplaceNamespacedDeployment;
jest.mock('#kubernetes/client-node', () => {
mockListNamespacedIngress = jest.fn();
mockCreateNamespacedDeployment = jest.fn();
mockReplaceNamespacedDeployment = jest.fn();
return {
KubeConfig: jest.fn().mockImplementation(() => ({
loadFromCluster: jest.fn(),
loadFromDefault: jest.fn(),
makeApiClient: () => ({
listNamespacedIngress: mockListNamespacedIngress,
createNamespacedDeployment: mockCreateNamespacedDeployment,
replaceNamespacedDeployment: mockReplaceNamespacedDeployment,
}),
})),
};
});
And running the test here
it.only('Should return a log of success if replaced deployment resolves', async () => {
mockCreateNamespacedDeployment.mockRejectedValueOnce(() =>
Promise.reject(new Error({ response: { statusCode: 409 } })),
);
mockReplaceNamespacedDeployment.mockResolvedValueOnce(true);
// When
await expect(createDeployment()).resolves.toEqual(true);
});
However on the console.log(JSON.stringify(e)), I am only ever getting undefined coming back. So it does seem to throw the error, but gives no error object from the catch. Here is my full stacktrace
● Kubernetes deployment script tests › Kubernetes Calls › Should return a log of success if replaced deployment resolves
expect(received).resolves.toEqual()
Received promise rejected instead of resolved
Rejected to value: [TypeError: Cannot read property 'statusCode' of undefined]
143 |
144 | // When
> 145 | await expect(createDeployment()).resolves.toEqual(true);
| ^
146 | });
147 | });
148 |
at expect (node_modules/expect/build/index.js:134:15)
at Object.it.only (src/__tests__/index.test.js:145:13)
console.log
Successful deployment
at createDeployment (src/index.js:73:13)
console.log
undefined
at createDeployment (src/index.js:76:13)

You can mock an async function throwing an Error like this
mockCreateNamespacedDeployment.mockRejectedValueOnce(new Error({ response: { statusCode: 409 } });
and then you can expect
await expect(createDeployment()).rejects.toThrow();
Btw in your async function you can replace all of the
- Promise.resolve(xxx)
+ return xxx
// and
- Promise.reject(xxx)
+ throw xxx

Related

Jest testing Promise.all assignment to variable when rejection

I have a Promise.all with async calls to three different services and assigns the resulting lists to three variables
const { getList1 } = require('./module1');
const { getList2 } = require('./module2');
const { getList3 } = require('./module3');
...
const funcToTest = async (params) => {
...
const [list1, list2, list3] = await Promise.all([
getList1(key),
getList2(key),
getList3(key),
]);
// do some stuff
//return promise
}
Here is the getList1 snippet
// ./module1
exports.getList1 = async (param1) => {
...
try {
// make a request that returns a list
return list;
} catch (err) {
if ( err.statusCode === 404) {
return [];
}
throw err;
}
};
To test the function, the Jest case should reject with an error when module1's getList1 request fails
const app = require('../../app');
const module1 = require('../../module1');
const module2 = require('../../module2');
const module3 = require('../../module3');
const nerdStore = require('../../nerdStore');
...
// setup stuff
jest.mock('../../module1');
beforeEach(() => {
module1.getList1
.mockResolvedValue([1,2,3]);
// setup more mocks, stuff .etc
});
afterEach(() => {
jest.clearAllMocks();
});
...
describe('Test funcToTest', () => {
it('should reject with an error when an module1 getList1 fails', async () => {
const errorMessage = 'An error occurred';
module1.getList1
.mockRejectedValue(new Error(errorMessage));
const result = await app.funcToTest(testParams);
await expect(result).rejects.toThrow(errorMessage);
});
The jest test case fails and keeps returning the following
Test funcToTest › should reject with an error when an module1 getList1 fails
An error occurred
120 |
121 | module1.getList1
> 122 | .mockRejectedValue(new Error(errorMessage));
expect(received).rejects.toThrow()
Matcher error: received value must be a promise or a function returning a promise
Received has value: undefined
123 |
124 | const result = await app.funcToTest(testParams);
> 125 | await expect(result).rejects.toThrow(errorMessage);
| ^
126 | });
Promise.allSettled([list1, list2, list3]).
then((results) => results.forEach((result) => console.log(result.status)));
I ran the above and it kept saying fulfilled for all
How do you resolve the undefined issue, mock and assert a rejected promise so that it makes the jest test pass?
Thanks

Writing tests for an async function with Jest

I am trying to test the following function:
import * as config from "./config.js";
export const state = {
recipe: {},
};
export async function loadRecipe(id) {
let result;
let data;
try {
result = await fetch(`${config.API_URL}/${id}`);
data = await result.json();
} catch (e) {
console.log(e);
}
console.log(result.status);
if (!result.status === 200) {
console.log("here");
throw new Error(`${data.message} (${result.status})`);
console.log("1here");
}
const { recipe } = data.data;
state.recipe = {
id: recipe.id,
title: recipe.title,
publisher: recipe.publisher,
sourceUrl: recipe.source_url,
image: recipe.image_url,
servings: recipe.servings,
cookingTime: recipe.cooking_time,
ingredients: recipe.ingredients,
};
}
Here are the tests I have written. I am using jest-fetch-mock to mock the global fetch function. If I comment-out the second test and run it, I get the expected results. Now I want to test if a bad id is entered. So I created a second test with bad data and am mocking the result from the API:
"use strict()";
import * as model from "../model.js";
import * as apiResponse from "../__fixtures__/apiResponse.js";
import * as recipes from "../__fixtures__/recipes.js";
beforeEach(() => {
fetch.resetMocks();
});
describe("Request from the api", () => {
test("Received valid data", async () => {
fetch.mockResponseOnce(
JSON.stringify(apiResponse.id_5ed6604591c37cdc054bca85)
);
const res = await model.loadRecipe("5ed6604591c37cdc054bca85");
expect(model.state.recipe).toStrictEqual(
recipes.recipe_5ed6604591c37cdc054bca85
);
expect(fetch).toHaveBeenCalledTimes(1);
});
test("Requested an invalid id", () => {
const body = apiResponse.invalid_5ed6604591c37cdc054bca85zzzzz;
const init = { status: 400, statusText: "Bad Request" };
fetch.mockResponseOnce(JSON.stringify(body), init);
expect(async () => {
await model.loadRecipe("5ed6604591c37cdc054bca85zzzzz");
}).toThrowError();
expect(fetch).toHaveBeenCalledTimes(1);
});
});
Whenever the second test is run I get the following error from yarn:
RUNS src/js/__tests__/model.test.js
node:internal/process/promises:225
triggerUncaughtException(err, true /* fromPromise */);
^
[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "TypeError: Cannot destructure property 'recipe' of '((cov_24wkscmv5p(...).s[10]++) , data.data)' as it is undefined.".] {
code: 'ERR_UNHANDLED_REJECTION'
}
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Please help me understand what is causing the issue.
Finally! I got it to work. I had to add rejects to catch the error. I got it from this page: https://eloquentcode.com/expect-a-function-to-throw-an-exception-in-jest
test("Requested an invalid id", () => {
const body = apiResponse.invalid_5ed6604591c37cdc054bca85zzzzz;
const init = { status: 400, statusText: "Bad Request" };
fetch.mockResponseOnce(JSON.stringify(body), init);
expect(async () => {
await model.loadRecipe("5ed6604591c37cdc054bca85zzzzz");
}).rejects.toThrowError();
expect(fetch).toHaveBeenCalledTimes(1);
});
Basically that error is due to your reject block/case.
So, when using async await, you could better keep it inside the try catch block to capture the reject case.
Consider following snippet for example -
var prom = function(p_param) {
return new Promise((resolve,reject) => {
setTimeout(()=>{
if(p_param%2 == 0){
resolve('Data good');
} else {
reject('Bad Data');
}
}, 3000);
});
}
async function runMain(p_data){
console.log('Verifying Data - '+p_data);
try {
var t = await prom(p_data); //resolve
console.log(t);
} catch(err) {
console.log(err); //reject
}
}
runMain(5);
This snippet would result in 'reject', Output:
Verifying Data - 5
Bad Data

How to test a recursive function is being called X amount of times using Jest? My method hangs forever if I use the spy method?

utils file
const isStatusError = (err: any): err is StatusError =>
err.status !== undefined;
export const handleError = async (err: any, emailer?: Mailer) => {
const sendErrorEmail = async (
subject: string,
text: string,
emailer?: Mailer
) => {
try {
const mail: Pick<Mail, "from" | "to"> = {
from: config.email.user,
to: config.email.user,
};
// 2. This throws an error
await emailer?.send({ ...mail, subject, text });
} catch (err) {
// 3. It should call this function recursively...
await handleError(new EmailError(err), emailer);
}
};
if (isStatusError(err)) {
if (err instanceof ScrapeError) {
console.log("Failed to scrape the website: \n", err.message);
}
if (err instanceof AgendaJobError) {
console.log("Job ", err.message);
// #TODO
}
if (err instanceof RepositoryError) {
console.log("Repository: ");
console.log(err.message);
// #TODO
}
// 4. and eventually come here and end the test...
if (err instanceof EmailError) {
console.log("Failed to create email service", err);
}
// 1. It goes here first.
if (err instanceof StatusError) {
console.log("generic error", err);
await sendErrorEmail("Error", "", emailer);
}
} else {
if (err instanceof Error) {
console.log("Generic error", err.message);
}
console.log("Generic error", err);
}
};
test file
import * as utils from "./app.utils";
import { Mailer } from "./services/email/Emailer.types";
import { StatusError } from "./shared/errors";
const getMockEmailer = (implementation?: Partial<Mailer>) =>
jest.fn<Mailer, []>(() => ({
service: "gmail",
port: 5432,
secure: false,
auth: {
user: "user",
pass: "pass",
},
verify: async () => true,
send: async () => true,
...implementation,
}))();
describe("error handling", () => {
it("should handle email failed to send", async () => {
const mockEmailer = getMockEmailer({
send: async () => {
throw new Error();
},
});
// This line is the problem. If I comment it out, it's all good.
const spiedHandleError = jest.spyOn(utils, "handleError");
// #TODO: Typescript will complain mockEmailer is missing a private JS Class variable (e.g. #transporter) if you remove `as any`.
await utils.handleError(new StatusError(500, ""), mockEmailer as any);
expect(spiedHandleError).toBeCalledTimes(2);
});
});
This test runs forever, and it is because I made handleError a spy function.
I tried to import itself and run await utils.handleError(new EmailError(err), emailer) but it still continue to hang.
So what happens is:
It throws an Error.
It will then figure out it is a StatusError which is a custom error, and it will output the error and call a function to send an email.
However, attempting to send an email throws another Error
It should then call itself with EmailError
It will detect it is an EmailError and only output the error.
Logic wise, there is no infinite loop.
In the utils file, if you comment this const spiedHandleError = jest.spyOn(utils, "handleError"); out, the test will be fine.
Is there a way around this somehow?
I realized it's my own logic that caused the infinite loop. I forgot to add the return statement to each of my if statement.
My spy function now works.
const spiedHandleError = jest.spyOn(utils, "handleError");
await utils.handleError({
err: new StatusError(500, "error"),
emailer: mockEmailer,
});
expect(spiedHandleError).toBeCalledTimes(2);
expect(spiedHandleError.mock.calls).toEqual([
[{ err: new StatusError(500, "error"), emailer: mockEmailer }],
[
{
err: new EmailError("failed to send an error report email."),
emailer: mockEmailer,
},
],
]);
It's impossible to spy or mock a function that is used in the same module it was defined. This is the limitation of JavaScript, a variable cannot be reached from another scope. This is what happens:
let moduleObj = (() => {
let foo = () => 'foo';
let bar = () => foo();
return { foo, bar };
})();
moduleObj.foo = () => 'fake foo';
moduleObj.foo() // 'fake foo'
moduleObj.bar() // 'foo'
The only way a function can be written to allow this defining and consistently using it as a method on some object like CommonJS exports:
exports.handleError = async (...) => {
...
exports.handleError(...);
...
};
This workaround is impractical and incompatible with ES modules. Unless you do that, it's impossible to spy on recursively called function like handleError. There's babel-plugin-rewire hack that allows to do this but it's known to be incompatible with Jest.
A proper testing strategy is to not assert that the function called itself (such assertions may be useful for debugging but nothing more) but assert effects that the recursion causes. In this case this includes console.log calls.
There are no reasons for spyOn to cause infinite loop. With no mock implementation provided, it's just a wrapper around original function. And as explained above, there's no way how it can affect internal handleError calls, so it shouldn't affect the way tested function works.
It's unsafe to spy on utils ES module object because it's read-only by specification and can result in error depending on Jest setup.

Not able to catch exception from promise, error response is undefined

I've got some basic Javascript code that calls a stock API with symbols where the symbols are provided from a simple HTTP call like this:
GET http://localhost:4000/batch_stock_prices/?stocks=12312.
I believe I am misunderstanding the syntax for how to catch an exception from a promise..
An exception gets thrown that 12312 is an invalid symbol which I expect, on the terminal running the node server I see the exception but I'm not able to pass it back in the HTTP response. The error that's passed back in the response is 'undefined'. How can I catch the exception? Do I need a try catch somewhere?
const fetch = require('node-fetch')
const { IEXCloudClient } = require("node-iex-cloud");
const { type } = require('tap');
const iex = new IEXCloudClient(fetch, {
sandbox: true,
publishable: "pk_2f78524e5........23c327e24b5",
version: "stable"
});
'use strict'
async function getCurrentPriceOfBatchStocks(_stock) {
stocks_to_submit = _stock['stocks'];
console.log(stocks_to_submit)
response = await iex
.batchSymbols(stocks_to_submit)
.price()
.catch(function (error) { // <-- doesn't seem to get called
console.log("Exception: " + error);
throw error;
})
return new Promise((resolve, reject) => {
if (response) {
resolve(response)
} else {
reject(response); // <-- response is undefined. why?
}
});
}
const batchStocksSchema = {
querystring: {
type: 'object',
properties: {
stocks: {
type: 'string'
}
},
required: ['stocks']
}
}
module.exports = async function (fastify, opts) {
fastify.get('/batch_stock_prices/', {
schema: batchStocksSchema
}, async function (request, reply) {
try {
var response = await getCurrentPriceOfBatchStocks(request.query)
// console.log(response)
return reply
.code(200)
.send(response);
} catch (e) {
console.log(e)
return reply
.code(400)
.send('Bad Request, exception: ' + e) // outputs: ...exception: undefined
}
})
}
It's hard to say for sure what's wrong without running the code, but there are several issues with the use of async, await, and promises in the code, and with creating implicit globals. (Also various missing ;.) If we sort those out, it may be that whatever error is occurring will stop being obscured. See *** comments:
"use strict"; // *** This has to be at the very beginning of the compilation
// unit, it can't be later in the code as it is in the question
const fetch = require('node-fetch')
const { IEXCloudClient } = require("node-iex-cloud");
const { type } = require('tap');
const iex = new IEXCloudClient(fetch, {
sandbox: true,
publishable: "pk_2f78524e5........23c327e24b5",
version: "stable"
});
async function getCurrentPriceOfBatchStocks(_stock) {
// *** Declare `stocks_to_submit`
const stocks_to_submit = _stock['stocks'];
// *** Declare `response`
const response = await iex.batchSymbols(stocks_to_submit).price();
// *** Don't catch the error, let it propagate; the caller should
// know whether the call succeeded or failed
// *** Don't use `new Promise`, there's no purpose to it
return response;
}
const batchStocksSchema = {
querystring: {
type: 'object',
properties: {
stocks: {
type: 'string'
}
},
required: ['stocks']
}
};
// *** This function never uses `await`, so don't make it `async`
module.exports = function (fastify, opts) {
fastify.get('/batch_stock_prices/', {
schema: batchStocksSchema
}, function (request, reply) { // *** Typically old-style callback APIs don't do
// anything with the promise an `async` function
// returns, so don't pass `async` functions into them
getCurrentPriceOfBatchStocks(request.query)
.then(response => {
// *** No `return` here, we aren't resolving the promise from `then` with the result
// of `send`
reply
.code(200)
.send(response);
})
.catch(e => {
console.log(e);
// *** No `return` here, we aren't resolving the promise from `catch` with the
// result of `send`
reply
.code(400)
.send('Bad Request, exception: ' + e);
});
});
};
For why the catch is not called in this part:
response = await iex
.batchSymbols(stocks_to_submit)
.price()
.catch(function (error) { // <-- doesn't seem to get called
console.log("Exception: " + error);
throw error;
})
and why response is undefined:
return new Promise((resolve, reject) => {
if (response) {
resolve(response)
} else {
reject(response); // <-- response is undefined. why?
} });
This is the cause:
The promise returned by price() call had resolves an undefined value (instead of rejecting with an error). Your "await" wait for this undefined value and assigned it to "response" variable.
The price() when having problem have already handled the error and then print the details in the console:
Error: <html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
at IEXRequest.<anonymous> (/home/runner/ArtisticAridSite/node_modules/node-iex-cloud/lib/request.js:128:35)
at step (/home/runner/ArtisticAridSite/node_modules/node-iex-cloud/lib/request.js:32:23)
at Object.next (/home/runner/ArtisticAridSite/node_modules/node-iex-cloud/lib/request.js:13:53)
at fulfilled (/home/runner/ArtisticAridSite/node_modules/node-iex-cloud/lib/request.js:4:58)
It wasn't really passing the error back in the chain to your code.
So on your question of "How can I catch the exception?". Unfortunately you probably has no way to receive the exception details (unless you can control the error handling behaviour of iex). You may consider to check whether the result is undefined and handle accordingly.

JavaScript - mocking console in Jest / mock "was not called"

I'm trying to mock console.info which I know will be called when an imported function runs. The function consists entirely of a single fetch which, when not running in production, reports the request and response using console.info.
At the question Jest. How to mock console when it is used by a third-party-library?, the top-rated answer suggests overwriting global.console, so I'm using jest.spyOn to try that out:
import * as ourModule from "../src/ourModule";
test("Thing", () => {
// Tested function requires this. Including it here in case it's causing
// something quirky that readers of this question may know about
global.fetch = require("jest-fetch-mock");
const mockInfo = jest.spyOn(global.console, "info").mockImplementation(
() => { console.error("mockInfo") }
);
ourModule.functionBeingTested("test");
expect(mockInfo).toHaveBeenCalled();
}
As expected, the output contains an instance of "mockInfo". However, then testing that with toHaveBeenCalled() fails.
expect(jest.fn()).toHaveBeenCalled()
Expected mock function to have been called, but it was not called.
40 |
41 | ourModule.functionBeingTested("test");
> 42 | expect(mockInfo).toHaveBeenCalled();
| ^
43 |
at Object.toHaveBeenCalled (__tests__/basic.test.js:42:22)
console.error __tests__/basic.test.js:38
mockInfo
I've tried moving the spyOn to before the module is loaded, as suggested in one of the comments on the answer, with no difference in result. What am I missing here?
Here's the function in question:
function functionBeingTested(value) {
const fetchData = {
something: value
};
fetch("https://example.com/api", {
method: "POST",
mode: "cors",
body: JSON.stringify(fetchData),
})
.then( response => {
if (response.ok) {
if (MODE != "production") {
console.info(fetchData);
console.info(response);
}
} else {
console.error(`${response.status}: ${response.statusText}`);
}
})
.catch( error => {
console.error(error);
});
}
Issue
console.info is called in a Promise callback which hasn't executed by the time ourModule.functionBeingTested returns and the expect runs.
Solution
Make sure the Promise callback that calls console.info has run before running the expect.
The easiest way to do that is to return the Promise from ourModule.functionBeingTested:
function functionBeingTested(value) {
const fetchData = {
something: value
};
return fetch("https://example.com/api", { // return the Promise
method: "POST",
mode: "cors",
body: JSON.stringify(fetchData),
})
.then(response => {
if (response.ok) {
if (MODE != "production") {
console.info(fetchData);
console.info(response);
}
} else {
console.error(`${response.status}: ${response.statusText}`);
}
})
.catch(error => {
console.error(error);
});
}
...and wait for it to resolve before asserting:
test("Thing", async () => { // use an async test function...
// Tested function requires this. Including it here in case it's causing
// something quirky that readers of this question may know about
global.fetch = require("jest-fetch-mock");
const mockInfo = jest.spyOn(global.console, "info").mockImplementation(
() => { console.error("mockInfo") }
);
await ourModule.functionBeingTested("test"); // ...and wait for the Promise to resolve
expect(mockInfo).toHaveBeenCalled(); // SUCCESS
});

Categories

Resources