Skip to content
Roberto's Corner

Exporting a function for Unit Testing purposes

javascript, unit testing, software engineering, rewire, export4 min read

Today, I bring you a topic which is many times unnoticed or neglected by developers in a daily basis, and I truly believe most of us have done or still do this practice on daily basis.

The practice I'm referring to it's exporting for unit testing purposes only. Usually if you export a function from your module just for testing, something may be smelling.

Why is it wrong?

Ideally, your module (eg. ES Modules) will export a set of functions or methods that represent its signature or public API. The first problem occurs right here, when exporting a private member of the module to be easier to unit test, you are turning this private member into a public one right away. This member can then be used by the client in ways you haven't thought of! It clearly rises red flags as both your client code and module will be more prone to errors and security flaws. As an overall, the cons will overweight the pros of exporting something private just for unit testing purposes.

What alternatives do I have?

There is also no silver bullet or a specific pattern to fix this problem. Each use case is unique and has its owns business requirements and complexity. Nevertheless, the aim for refactoring should be focused on Single Responsibility Principle and Dependency Inversion. These principles are known to improve the code testability and quality.

All units of a modular system must do a single job only, and do it right!

How do I refactor my code?

As rule of thumb, when I face such need of exporting a private function just for testing, I ask myself the following questions:

1. Can I cover a private function by consuming the public api?

Providing distinct inputs to the module's public API must be enough to cover private/internal functions.

2. Should I refactor the code to make testing easier?

If I can't fully cover the private function with the public API, it can be an indicator that something is smelling. I would spend some time to re-think the implementation and perhaps explore software patterns like Dependency Injection. With DI, our code dependencies are really easy to test as you can inject spies directly into the implementation.

3. Can a private function be refactored to be public?

If the refactor didn't help or wasn't possible, then we can check if this private function provides useful functionality and if it's safe to be exported.

4. Does a private function really belongs to this module?

If it's safe to be exported, perhaps it could belong to another module, or even become a module itself. Then we can be the client of this module and import it as a dependency.

5. Should I consider to use "monkey-patching" technique?

In Javascript ecosystem, we can use rewire package to inspect private functions or variables, it will export private functions and let you spy or stub them. A similar technique is also present in Deno land for mocking purposes. Deno suggest to export an _internals object with private methods to let us monkey-patch and spy those internal functions.

Example of concerns

In the following example, a naive ES module purpose is to provide two members to append and remove elements of breadcrumbs:

const SEPARATOR = ' > ';
let currentPath = '';
const withSeparator = (element) => `${SEPARATOR}${element}`;
const appendToCurrentPath = (value) => {
currentPath = `${currentPath}${value}`;
}
export const appendElement = (newElement, separator = false) => {
if (separator) {
appendToCurrentPath(withSeparator(newElement))
} else {
appendToCurrentPath(newElement)
}
}
// eg another public method would be
export const removeLastElement = () => { /* implementation */}

In this naive example, we don't want to export withSeparator as it is a module private member. The way we're going to test it, it's by just by calling appendElement with separator flag set to true. The public member will cover the internal code needed.

Although, there is no big risk in exporting the withSeparator as it is a pure function that doesn't mutate state, if you export appendToCurrentPath then it's a different story altogether. This one mutates the state and if it's wrongly used by a client, it could break the module execution when calling public APIs like removeLastElement.

Thoughts

A good software design takes account the implementation of unit tests and make sure the code is easy to test. That doesn't mean to export private code, it means we export a good public API that it's easy to test.

Regardless of some techniques or technologies that favor and accept exporting private members (see Deno _internals or rewire above), imho, it brings more risks than benefits specially for a big codebase where the complexity is considerably higher.

I hope this article helped you to understand the main issue and gave you hints on how to improve your code. Please tweet me and share your feedback so that we can learn together.

References:
  1. Dependency Injection
  2. Deno Mocking
  3. Rewire
author @RobertoRJ

Thanks for reading my article. I am Roberto and I'm based in Madeira Island, Portugal. Get in touch via @RobertoRJ or email me at em.susejotrebor@tniopyrtne

© 2024 by Roberto's Corner - Blog posts about Web and Software Engineering
Built with Gatsby - Theme by LekoArts