Santiago de León


Structure Your JavaScript Code for Testability

EDIT: After spending a lot more time testing JavaScript apps, I see this post as being a naive attempt at simplifying a topic that is a lot more nuanced. The ideas exposed here may still be useful for beginners but may be impractical in some cases.

“Code without tests is broken by design” — Jacob Kaplan-Moss

Testing is not optional. It is not a time-consuming task that you can shave off to gain productivity. As a developer, it is your responsibility to deliver quality software and that quality cannot be guaranteed without automated tests.

That being said, there are situations where testing is not trivial unless you have a few tricks up your sleeve. These hard-to-test pieces of code usually involve side effects or global variables that take different values in different environments. In this post, I will show some of these problems in code examples and propose a coding pattern to alleviate a lot of that testing pain in JavaScript.

Code structure

Imagine you are writing a React Native component (you don’t need to know React Native to understand any of this). Your component needs a function that, given a width in px, calculates the corresponding percentage of screen real estate. I like to keep most of my logic outside of my React components, so let’s create an independent JavaScript module for that:

// Scaling.js
import { Dimensions } from 'react-native';
export const widthInPxToPercentage = (widthInPx) => {
const screenWidth = Dimensions.get('window').width;
return (widthInPx * 100) / screenWidth;

The function works, but it’s hard to test because it is impure. A pure function is a function that always returns the same output for the same input. Conversely, an impure function is one that may have side effects or depends on conditions from the outside to produce a value. In order to make our function pure, we can refactor it to take the screen width as an argument. This technique is known as dependency injection (DI). Don’t get scared by the fancy term, though. We won’t be using dependency injectors or any sophisticated dependency management mechanism. Our refactored code would look like this:

// Scaling.js
import { Dimensions } from 'react-native';
export const widthInPxToPercentage = (screenWidth, widthInPx) => (widthInPx * 100) / screenWidth;

This is much more testable! However, with this approach, all clients of our module will need to pass the screen width in. What I like to do in these cases is separate the implementation from the API using two functions:

// Scaling.js
import { Dimensions } from 'react-native';
export const widthInPxToPercentageImplementation =
(screenWidth, widthInPx) => (widthInPx * 100) / screenWidth;
export const widthInPxToPercentage =
widthInPx => widthInPxToPercentageImplementation(Dimensions.get('window').width, widthInPx);

The implementation function is a pure function that can easily be tested and is not visible outside the module (more on that later, don’t worry about that extra export). In this example, it is very simple but in the real world, you’ll often find yourself writing these as higher order functions since some dependencies will turn out to be functions.

The entry point is a function exposed to the outside-world that invokes the implementation passing the dependencies in, so that clients don’t need to worry about them. These are typically one-liners that you don’t need to unit test (although your integration tests will most likely cover them).

Now, let’s add our test:

// Scaling.spec.js
import { widthInPxToPercentageImplementation } from './Scaling';
test('test 10px is 10% of 100px wide screen', () => {
// Given
const screenWidth = 100;
const widthToConvert = 10;
    // When
const widthInPct = widthInPxToPercentageImplementation(screenWidth, widthToConvert);
    // Then

You have now made your code testable by using DI!

Directory structure and visibility

Some people put their tests in a __tests__ directory in the project's root. I don't like that approach because it's not portable: If you copy code to another project, you need to grab the implementation and the tests from completely separate places. Instead, what I like to do is put my tests in the same folder as my code (notice the relative import in the snippet). This makes your test code more portable and encourages developers on your team to treat tests as an integral part of the development process.

Also, you may notice that in Scaling.js I exported both the implementation function and the entry point. The entry point needs to be visible to the outside world, but the implementation doesn't. However, I have no means of using the implementation function inside tests unless I export them, so I have to export both. In order to better organize the visibility of the module, I use an index.js file alongside the implementation and test files. In this file you can specify the API you want to expose to the outside world by using an export ... from statement. Our index.js file should look like this:

export { widthInPxToPercentage } from './Scaling';

This approach has two drawbacks that aren’t deal-breakers but you should be aware of: It creates a little bit of duplication and it doesn’t prevent people from accessing the module from the outside world.

The duplication is not terrible, although I must admit it does feel a little like maintaining .h files. The visibility shouldn't be a problem if your team is aware of this convention and you use references to the directory instead of the implementation file when you import the module. That is, import 'src/Scaling' instead of src/Scaling/Scaling: When you import a folder with an index.js file, JavaScript will import the index.js file impliscitly.

PRO Tip: If you're a Vim user you can run :r !grep export ./Scaling.js | grep -v Implementation inside your index.js file to automatically grab the functions you want to expose (you'll obviously need to edit that output to make a proper export statement).

By now, your module is complete and looks like this:

├── Scaling.js
└── Scaling.spec.js

You now have a pattern to structure your code in a testable manner while encouraging others to jump on board with the same practice. This is by no means a one-size-fits-all solution and your team might need custom conventions, but the important thing is that they exist.

What do you think about this pattern? Do you know of a better way to solve this problem? Let me know in the comments or on Twitter @bug_factory.

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!

More by Santiago de León

Topics of interest

More Related Stories