Do not test private functions. Test your public API.
If private functions require testing, you can use one of three approaches I know.
Make them part of your public API, the API you already test.
Return data that includes the input as well as the computed output. Update the public functions that use them to accept these structures instead of blindly accepting values. You can pass those back in the output from the public functions.
However, now we're passing whole I/O structures arounds. That can work, but it seems like overkill.
If you find you must test private functions, but don't want to (or just can't) export them as public functions, there is a way.
If you have access to the source file (not the module) as text, you can pass the file's text content plus your test functions as a concatenated string in the body (last) param to JavaScript's Function
constructor (a form of eval
).
Here's a proof of this concept.
var api = (function() {
// private
function double(value) {
return value * 2;
}
// public
return function api (value) {
return +(value) === +(value)
? double(value)
: { toString() { return `param [${value}] is not functionally a number` } };
}
})();
var suite = (function() {
function testPublic() {
console.log("testing public function, api");
console.log(api('bonk').toString()); // should see param message
console.assert(api(2) === 4, "public function api(2) == 4 should pass");
console.assert(api(3) === 4, "public function api(3) == 4 should fail");
}
function testPrivate() {
console.log("testing private function, double");
console.log(double('bonk').toString()); // should see "NaN"
console.assert(double('x') === 10, "private function double('x') === 10 should fail");
console.assert(double(5) === 10, "private function double(5) === 10 should pass");
}
// Add this string variable assignment of a function containing calls to your public and private as an IIFE.
var tests = `
(function () {
testPublic();
testPrivate();
}());
`;
// return the string concatenation of the functions and the test IIFE.
return [ testPublic, testPrivate, tests ].join(";\n")
})();
/* test it out */
/*
* @function exec encapsulates our join and run logic:
* + join api and suite
* + pass as body param to Function()
* + exec on 0 as context (this) so it does not leak.
*/
function exec(...parts) {
var body = [...parts].join(";\n");
var exec = Function(body);
exec(0);
}
exec(api, suite);
/*
// Should see the following console output
testing public function, api
param [bonk] is not functionally a number
Assertion failed: public function api(3) == 4 should fail
testing private function, double
NaN
Assertion failed: private function double('x') === 10 should fail
*/
In Node.JS, we would require
or import
the test library, then read the source file with a Node.JS FileReader
, do the same for the test file, and pass their contents to our exec()
function.
In a browser test page we could load the testing library, then xhr|fetch the source, then xhr|fetch the test, then concatenate their contents using the exec()
function.
A major problem with this approach comes up when a module under test requires other dependencies. You'll then need a bundler like browserify or a server-client-agnostic bundler to resolve the require|import paths and concatenate those, too. I don't think I'd want to do that.
So, yes, there's more process involved, it will be slower as it is an additional build step, but, yes, it can be done.