如何对非导出函数进行单元测试?

在 JavaScriptES6模块中,可能有许多小的、易于测试的函数应该被测试,但不应该被导出。如何在不导出函数的情况下测试模块中的函数?(不使用 重新接线)。

31609 次浏览

Export an "exportedForTesting" const

function shouldntBeExportedFn(){
// Does stuff that needs to be tested
// but is not for use outside of this package
}


export function exportedFn(){
// A function that should be called
// from code outside of this package and
// uses other functions in this package
}


export const exportedForTesting = {
shouldntBeExportedFn
}

The following can be used in production code:

import { exportedFn } from './myPackage';

And this can be used in unit tests:

import { exportedFn, exportedForTesting } from './myPackage';
const { shouldntBeExportedFn } = exportedForTesting;

This strategy retains the context clues for other developers on my team that shouldntBeExportedFn() should not be used outside of the package except for testing.

I've been using this for years, and I find that it works very well.

I wish I had a better answer for you, Jordan. 😊 I had very similar question in both JavaScript and C# contexts in the past...

Answer / not answer

At some point I had to embrace the fact that if I want granular unit tests that cover unexported/private functions/methods, I really should expose them. Some people would say that it's a violation of encapsulation, yet others disagree with that. The former group of people would also say that until a function is exported/public, it's essentially an implementation detail, thus should not be unit-tested.

If you're practicing TDD then Mark Seeman's explanation should be relevant (Pluralsight), and hopefully it will clarify why it's okay to expose things.

I don't know if you can find some trick for invoking the unexported functions directly from your unit tests without making changes to the code under test, but I would not go that way personally.

Just an option

Another option is to split your library into two. Say, library A is your application code, and library B is the package that contains all those functions you would like to avoid exporting from A's interface.

If they are two different libraries, you can control on a very fine level what is exposed and how it is tested. Library A will just depend on B without leaking any of the B's details. Both A and B are then testable independently.

This will require different code organization, sure, but it will work. Tools like Lerna simplify multi-package repositories for JavaScript code.

Side note

I don't agree with AlexSzabó, to be honest. Testing the non-exported function by testing the function(s) that use it is not really unit-testing.

Maybe necro-posting but the way I attacked this problem is by using an 'index.js' which exports only the function(s) you want to be made public.

You still have to export the private functions, but this way does add a layer of abstraction between testing and production.

module/startingFile.js

function privateFunction1() {/**/};
function privateFunction2() {/**/};


// Different syntax is a good visual indicator that this is different to public function
exports.privateFunction1 = privateFunction1;
exports.privateFunction2 = privateFunction2;


exports.publicFunction1 = function() {/**/};
exports.publicFunction2 = function() {/**/};

module/index.js

exports.publicFunction1 = require('./startingFile.js').publicFunction1;
exports.publicFunction2 = require('./startingFile.js').publicFunction2;

ImportingFile.js

const { publicFunction1, publicFunction2 } = require('./module');

You could even use the NODE_ENV variable to only export the private functions when not in production.

How To Test Private Functions In An Express Route

In my case I wanted to write unit tests for private functions in an Express route.

The Problem

If we do something like this, the test works fine:

const exportedForTesting = {
privateFunction1,
privateFunction2,
};


module.exports = {
router,
exportedForTesting,
};

However, Express complains about it: TypeError app.use() requires a middleware function

... because it expects to see simply:

module.exports = router;

The Solution

We need to add the map as a property on router:

router.exportedForTesting = {
privateFunction1,
privateFunction2,
};


module.exports = router;

Then we can access the private functions in our test like Jordan's answer.

const { exportedForTesting } = require('path/to/route/file.js');


const { privateFunction1, privateFunction2 } = exportedForTesting;

I know this already has an answer, but I didn't like the answer selected because it involves having extra functions. :) After some research lead me to https://www.jstopics.com/articles/javascript-include-file (so basic idea can be found there and credit to him/her). My code is Typescript, but this should work for normal Javascript too.

Assuming your source app is in "app.ts" and you have a private function called "function1":

// existing private function, no change here
function function1(s :string) :boolean => {
let result=false; /* code, result=true if good */ return result;
}


// at bottom add this new code
 

// exports for testing only
// make the value something that will never be in production
if (process.env['NODE_DEV'] == 'TEST') {
module.exports.function1 = function1;
module.exports.function2 = function2; // add functions as needed
}

I'm using Jest to do unit testing, so in tests/app.test.ts we do:

process.env['NODE_DEV'] = 'TEST';
let app = require("../app");


describe("app tests", () => {
test('app function1', () => {
expect(app.function1("blah")).toBe(true);
expect(app.function1("foo")).toBe(false);
});


test('app function2', () => {
expect(app.function2(2)).toBeUndefined();
});
});

Add whatever tests you need and it will test those private functions. It works for me without needing Rewire.