Javascript Named Exports for testing

I'm now using Jest and its useful mocking extensions quite often at work, and while the combination of modern Javascript + NodeJS+ Typescript + third-party packages + your code sometimes makes things quite a challenge to test, with effort (and a few refactors to ease testability) most times I feel satisfied with the ending results.

Javascript imports via import (MDN reference) are quite flexible, but on the other hand being modules means that you either export something, or you don't and then is not available from the outside, so it's not as easy to test the unexposed code fragments. A colleague at work explained to me a testing pattern for that scenario, but I haven't found it online, so this small post is both a personal note and a documentation attempt.

The Testables Named Export Pattern

The main idea is using javascript named exports to export a specific group for testing with a meaningful name (e.g. testables). This way, you do add one additional export, but only one, also clearly signalling the intention; and you don't alter logic elsewhere in the code.

Let's use a tiny example:

a-file.ts

export function aMethod() { 
  ... 
  const result = aNonExportedMethod('something');
  anotherNonExportedMethod();
  ...
}

export function anotherMethod() { ... }

aNonExportedMethod(param : string) { ... }

anotherNonExportedMethod(); { ... }

export const testables = {
  aNonExportedMethod,
  anotherNonExportedMethod,
};

Your normal code will do normal imports, and simply won't care about testables nor be affected by it:

another-file.ts

import { aMethod, anotherMethod } from './a-file';

...

But when writing your tests, you will now be able to also import the testables group, so that you can write tests focused on that logic:

a-file.test.ts

import { aMethod, anotherMethod, testables } from './a-file';

...

const result = testables.aNonExportedMethod('a-string');
expect(result).toEqual(...);

Closing Notes

I am assuming no default export, but the behaviour is pretty much the same otherwise. Just don't return the testables as the default export if you use it (I don't fancy much default exports, and seems I'm not the only one).

Be careful as module logic will still execute upon import (any Javascript/Typescript outside functions will run) no matter if you only import { testables }. This can bite you but in general it is a good practice to avoid "import work" so keep it in mind.

I know not everyone will like this approach and could argue that you should only test the visible surface (even more so if the non-exported methods are called inside the exported ones). But I at least prefer smaller, more focused tests so, in my example, after testing testables.aNonExportedMethod and testables.anotherNonExportedMethod the test(s) about aMethod would be much smaller and focused on the logic not already covered by others.

Tags: Development Javascript Patterns & Practices Testing Typescript

Javascript Named Exports for testing article, written by Kartones. Published on