How can I unit test non-exported functions?
Asked Answered
S

8

113

In a JavaScript ES6-module, there may be many, small, easy-to-test functions that should be tested, but shouldn't be exported. How do I test functions in a module without exporting them? (without using Rewire).

Sofia answered 9/1, 2019 at 18:13 Comment(5)
If they're not exported, that means they're not public, right? I assume in that case they are being used by the publicly exported function/class/whatever, so you can test them through what's publicly available.Oletaoletha
@AlexSzabó Yes they are, but I'm looking for a way to test them directly. The public function that uses them (in my case) is a complicated generator function that makes it difficult to test them through the public functionSofia
First of all, sorry that I won't be able to help you in any meaningful way :( - but exposing private functions just for the sake of testing is not considered a good practice. If the functions are generic enough, you could expose them publicly with explicit unit tests, but if they're not, then you're making your tests highly coupled with the implementation details, so they can break (for example) on refactoring without any real change in the behaviour.Oletaoletha
@AlexSzabó Yes, I whole-heatedly agree with you. Thanks for you input! This is probably just an issue that i'm running into due to the particularly complex tools that I'm having to use for this project.Sofia
I need to find a way to do this too. Testing the functionality of the private methods by exercising the public one is not a very meaningful unit test. That public method could call a ton of private ones, and when it fails, you won't know where or why.Bloodshot
S
142

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.

Sofia answered 9/1, 2019 at 18:14 Comment(3)
I'm afraid this is the only answer. A reflection class would be handy. Something that would allow you to import a module definition file and get access to the non exported fields.Bloodshot
This is a practical idea but it's exporting anyway. It might be better if exported conditionally like using process.env.NODE_ENV === 'test'Forsook
@Kennyhyun, interesting idea! With webpack's DefinePlugin and UglifyJS removing if('test' === 'test'){ I think that could work - it'd just have to output is that has the export at top level.Sofia
D
19

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.

Danged answered 9/1, 2019 at 18:36 Comment(6)
I agree that "private functions are an implementation detail" and it's not them that they need to be tested, but the behaviour of your publicly available function, class or whatever you have at hand. Like you said, when you are doing TDD, you have a unit that you intend to use somewhere else, and you keep adding tests to describe what it should do given certain conditions - therefore testing its behaviour, not it's internals.Oletaoletha
@AlexSzabó I recommend you to watch the Mark Seeman's video I put a link to. He explains that with TDD testing "internals" may be totally fine. Over the years I more and more find proofs that "internals" vs "interface" is very relative in the context of unit-testing.Danged
Testing implementation detail is not unit testing. That's what you're doing when you test non-exported functions.Caenogenesis
@Caenogenesis that way of thinking is overly puristic and unpragmatic. There's nothing wrong in testing the implementation if that is critical to unit's operation. Even if technically isn't unit-testing, it can assist unit-testing greatly. There are other options, of course, like method extraction which may look better and purer, but not always easy to do.Danged
@IgorSoloydenko On the contrary. I came to that conclusion for 100% pragmatic reasons. The reason testing non-exported function is unpragmatic is that refactoring will cause your unit test to fail. Which means not testing unexported functions is more pragmatic. Your unit tests are there to give you confidence that your refactoring is not breaking your logic. Another pragmatic reason for not testing unexported function is you can use test coverage to discover code that is never reached. If you force the code to be tested it is always reached but artificially.Caenogenesis
@IgorSoloydenko I use my tests as a tool to help me write code. Which is why I have this opinion. If you are only using your tests because you have a checklist to tick so that your boss is happy then fine, test unexported code. But be aware your unit tests have just become less useful.Caenogenesis
J
18

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.

Johnston answered 9/6, 2022 at 22:26 Comment(2)
This is the best answer imho, it doesn't break encapsulation and still exposes functions only for tests. Side note: You can use cross-env lib to explicitly and globally set NODE_ENV to test for testing purposes.Shackelford
Downside is that a code editor does not understand this so I would complain that the function is not exported.Whitver
G
8

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.

Gisele answered 1/11, 2021 at 17:8 Comment(1)
That seems like a good solution for a public module :)Sofia
R
7

For ES6. If you are using Jest "NODE_ENV" is set for you.

export let exportsForTesting;
if (process.env.NODE_ENV === 'test') {
  exportsForTesting = { symbolToExport };
}
Reinold answered 3/3, 2023 at 2:31 Comment(0)
M
3

To test private functions, you can test inline, in the same file. The testing part will be removed during bundling. Some test framework such as Vitest allow this.

Misinform answered 16/1, 2023 at 10:54 Comment(0)
A
3

Perhaps try a conditional export like :

a.js

module.exports = {
  foo,
  foo1: process.env['NODE_DEV'] == 'TEST123' && foo1
}

a.test.js

process.env['NODE_DEV'] = 'TEST123'

const { foo1, foo } = require('./a')

This is would ensure that the tested function is also included in the test coverage.

Aulea answered 28/3, 2023 at 2:37 Comment(0)
B
1

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;
Bespatter answered 9/6, 2022 at 19:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.