I am trying to write a unit test for a function but cannot figure out how to check if it makes a call to a nested function with a specific argument. I am assuming I will need to use sinon alongside chai and mocha for this, but I could really use some help.
The function I would like to test looks like:
function myFunc(next, value) {
if (value === 1) {
const err = new Error('This sets an error');
next(err);
} else {
next();
}
}
I would like to test if next is called with or without the err variable. From what I read so far I should use a spy for this (I think) but how would I use that spy? Looking at this example from the Sinon docs it is unclear to me where PubSub comes from:
"test should call subscribers with message as first argument" : function () {
var message = "an example message";
var spy = sinon.spy();
PubSub.subscribe(message, spy);
PubSub.publishSync(message, "some payload");
sinon.assert.calledOnce(spy);
sinon.assert.calledWith(spy, message);
}
Source: https://sinonjs.org/releases/latest/assertions/
If you have a function like this
function myFunc(next, value) {
if (value === 1) {
const err = new Error('This sets an error');
next(err);
} else {
next();
}
}
The test could look like this
it ('should call the callback with an Error argument', function (done) {
const callback = (err) => {
if (err && err instanceof Error && err.message === 'This sets an error'){
// test passed, called with an Error arg
done();
} else {
// force fail the test, the `err` is not what we expect it to be
done(new Error('Assertion failed'));
}
}
// with second arg equal to `1`, it should call `callback` with an Error
myFunc(callback, 1);
});
so you don't necessarily need sinon for that
Related
Let's say I have a function like this:
const getPlayer = (id) => {
return players[id;]
}
//--------------------------
const client = getPlayer(9);
How can I return the err parameter to the client variable if no player is found? For example:
if (client.err) {
//do something
}
I tried passing the error via throw new Error('my error') , but the function still doesn't get it, what am I doing wrong?:(
So your first instinct was correct, you should use the 'throw' keyword to raise an error. To act on the error you need to use try/catch like I've done below.
const getPlayer = (id) => {
if(id in players) {
return players[id];
}
throw new Error("Oh noes...!");
}
try {
const client = getPlayer(9);
} catch(error) {
console.log(error.message);
}
When an error is thrown inside a function being executed in a try block, execution immediately jumps to the catch block, allowing you to respond to the error appropriately.
Checkout try/catch syntax for that.
For example:
const getPlayer = (id) => {
if (!id) {
throw new Error('no id provided');
}
return players[id]
}
To get this "error" state, when it triggers you can do following:
try {
const client = getPlayer(null);
} catch(error) {
console.log(error.message);
}
I have tried something but not sure if this is what you are after:
let a = (x) => {
if (x == 0) {
throw new Error("Votes are zero");
} else {
return x;
}
};
Run it in the console with the values as a(0) --> will throw you a new error and a(5)
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.
What version of async are you using?
2.6.1
Which environment did the issue occur in (Node version/browser version)
8.11.3
What did you do? Please include a minimal reproducible case illustrating the issue.
Assuming fileObj is supplied from outside:
async.auto({
download: (downloadCB) => {
if (fileObj) {
fs.writeFile(__dirname + ‘fileNew.txt’, fileObj.content, 'base64', function (err) {
if (err){
return downloadCB(err);
}
return downloadCB(null , fileObj.generatedFileName); // works fine
});
} else {
let err = new Error('File not found');
return downloadCB(err);
}
},
collectData: ['download', async (results, collectCB) => {
console.log(typeof collectCB); // prints undefined
console.log(typeof results); // prints correct object
let res = await anHttpRequest();
if (res.response && res.response.statusCode == 200) {
return collectCB(null , 'fileCombined.txt'); // This is where the ISSUE happens
}
else if(res.response.statusCode >= 300) {
return collectCB(new Error('Request failed inside async-auto'));
}
}],
filterData: ['collectData', (results, filterCB) => {
doFilter(results.collectData, filterCB);
}],
})
What did you expect to happen?
After collectData finishes execution, filterData should begin execution the param passed inside collectCB function
What was the actual result?
TypeError: collectCB is not a function.
The same code executes well with version 2.0.1 but after upgrade to 2.6.1 it has stopped working and its critical for us. Any work arounds will also be appreciated.
based on the documentation (quoted in the other answer already but here it is again)
Wherever we accept a Node-style async function, we also directly accept an ES2017 async function. In this case, the async function will not be passed a final callback argument, and any thrown error will be used as the err argument of the implicit callback, and the return value will be used as the result value. (i.e. a rejected of the returned Promise becomes the err callback argument, and a resolved value becomes the result.)
what you would do is
async.auto({
download: (downloadCB) => {
if (fileObj) {
fs.writeFile(__dirname + ‘fileNew.txt’, fileObj.content, 'base64', function(err) {
if (err) {
return downloadCB(err);
}
return downloadCB(null, fileObj.generatedFileName); // works fine
});
} else {
let err = new Error('File not found');
return downloadCB(err);
}
},
// Note, no callback as per documentation
collectData: ['download', async (results) => {
console.log(typeof results); // prints correct object
let res = await anHttpRequest();
if (res.response && res.response.statusCode == 200) {
// this return is equivalent to callback(null, value);
return 'fileCombined.txt';
} else if (res.response.statusCode >= 300) {
// this throw is equivalent to callback(err);
throw new Error('Request failed inside async-auto');
}
// but surely something should be here!? for status code 201-209?
}],
filterData: ['collectData', (results, filterCB) => {
doFilter(results.collectData, filterCB);
}],
})
Just a copy-paste from the official documentation:
Wherever we accept a Node-style async function, we also directly
accept an ES2017 async function. In this case, the async function will
not be passed a final callback argument, and any thrown error will be
used as the err argument of the implicit callback, and the return
value will be used as the result value. (i.e. a rejected of the
returned Promise becomes the err callback argument, and a resolved
value becomes the result.)
I am facing trouble to get pass a test by running mocha which seems to be passing.
The test:
describe('.get()',function() {
it('should be called once',function() {
// => Need to spy on this
var callback = function(err,data) {
console.log('I am called');
if (err) {
console.log('I am logging the error '+err);
} else {
console.log('I am logging the data '+data);
}
}
agentMock._response = {body:'something',headers:'headers'};
// => Start Spying
var spy = sinon.spy(callback);
sinon.spy(agentMock,'get');
baseRequest.get(spy); // refer (a) below
expect(agentMock.get).to.have.been.calledOnce;
expect(spy).to.have.been.calledOnce;
expect(spy).to.have.been.calledWith(null,'data');
});
});
I want to test whether the callback is called or not. Therefore, I logged in the body of the callback, and the stdout also suggests it's being called.
The stdout :
.get()
1) should be called once
I am called
I am logging the data something
0 passing (15ms)
1 failing
1) .get() should be called once:
AssertionError: expected spy to have been called exactly once, but it was called 0 times
Details:
(a) baseRequest.get return the data as a bluebird promise. This can be used by passing in a nodeback to .get itself or by chaining .then after .get call.
BaseRequest.prototype.get = function(callback) {
// inner details
return invokeandPromisify(request,callback);
}
function invokeandPromisify(request, callback) {
return new Promise(function(resolve,reject) {
// Invoke the request
request.end(function(err,result) {
// Return the results as a promise
if (err || result.error) {
reject(err || result.error);
} else {
resolve(result);
}
});
}).nodeify(callback); // to have a node style callback
}
Does it happen because the callback on which I want to spy is passed to a different function( invokeandPromisify here ) and the spying is lost ? I am just interpreting this.
Regards.
Since baseRequest#get returns a promise, I would make the assertions after the promise is resolved.
See example below:
it('should be called once',function(done) {
// => Need to spy on this
var callback = function(err,data) {
console.log('I am called');
if (err) {
console.log('I am logging the error '+err);
} else {
console.log('I am logging the data '+data);
}
}
agentMock._response = {body:'something',headers:'headers'};
// => Start Spying
var spy = sinon.spy(callback);
sinon.spy(agentMock,'get');
baseRequest.get(spy).finally(function() {
expect(agentMock.get).to.have.been.calledOnce;
expect(spy).to.have.been.calledOnce;
expect(spy).to.have.been.calledWith(null,'data');
done();
});
});
Your test should be set as async by adding done. Then in your callback funtion call done()
Please check http://mochajs.org/#asynchronous-code
I am using async in a Nodejs application to run a series of functions in series. Each function calls the callback with an err (Can be null) and a result.
What I want to do is check in the series callback for a particular error ... a custom error:
async.series({
one: function(callback){
// Doing Stuff
if(No good)
callback(new error('Custom Error'), 'Failure');
else
callback(null, 1);
},
two: function(callback){
// Doing Stuff
if(No good)
callback(new error('Custom Error 2'), 'Failure');
else
callback(null, 2);
}
},
function(err, results) {
if(err) {
// Test for which error was thrown above
}
});
Any ideas on creating custom error and then testing for them in this context would be great.
You could make a custom error class:
var util = require('util');
var MyError = function (msg) {
MyError.super_.apply(this, arguments);
};
util.inherits(MyError, Error);
// use it in your callback
callback(new MyError(), ...);
// check for it
if (err instanceof MyError) {
...
};