Testing Non-Exported Functions in JavaScript

samanthaming

Samantha Ming

Posted on October 10, 2020

Testing Non-Exported Functions in JavaScript

Alt Text

Recently, I finally integrated unit testing into my startup project. I've settled with Jest, I'll speak more about this in a separate journal entry. While writing my test, I ran into a bit of a dilemma of trying to write unit tests for non-exported functions šŸ˜–

Testing Exported Function

It's super straightforward to test exported functions.

// utils.js
export function sayHi() {
  return 'šŸ‘‹';
}
Enter fullscreen mode Exit fullscreen mode

And a unit test could be something like this:

// utils.test.js

import { sayHi } from './utils.js';

describe('sayHi', () => {
  it('returns wave emoji', () => {
    expect(sayHi()).toBe('šŸ‘‹');
  });
});
Enter fullscreen mode Exit fullscreen mode

Non-export function

Now, what if the function is not exported?

function saySecret() {
  return 'šŸ¤«';
}
Enter fullscreen mode Exit fullscreen mode

Ah yikes, there is no way to test it! šŸ¤·ā€ā™€ļø

// utils.test.js

// āŒ
import { saySecret } from './utils.js';

saySecret; // undefined
Enter fullscreen mode Exit fullscreen mode

Introducing Rewire

And then I discover this nifty package called Rewire! Here's their official description:

Rewire adds a special setter and getter to modules so you can modify their behaviour for better unit testing. You may

  • inject mocks for other modules or globals like process
  • inspect private variables
  • override variables within the module.

The second point is exactly what I needed!

Installing Rewire for a Vue app

Instead of using rewire, I used a package called babel-plugin-rewire. Which is essentially ES6 version of rewire, so I can use import. Here's their description:

It is inspired by rewire.js and transfers its concepts to es6 using babel.

Step 1: Install package

# Yarn
yarn add -D babel-plugin-rewire

# Npm
npm install babel-plugin-rewire --save-dev
Enter fullscreen mode Exit fullscreen mode

Step 2: Add to babel config

babel.config.js

module.exports = {
  plugins: ['babel-plugin-rewire'],
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Using it

Alright, now that it's installed, let's revisit our non-exported function.

function saySecret() {
  return 'šŸ¤«';
}
Enter fullscreen mode Exit fullscreen mode

And now, we can use rewire to fetch our non-export function:

// utils.test.js

import utilsRewire from './utils.js';

describe('saySecret', () => {
  it('returns shh emoji', () => {
    const saySecret = utilsRewire.__get__('saySecret'); // šŸ‘ˆ the secret sauce

    expect(saySecret()).toBe('šŸ¤«');
  });
});
Enter fullscreen mode Exit fullscreen mode

Non-exported function must be called in Exported Function

One important thing I need to point out! In order to test the non-exported function, it needs to be used in an exported function.

āŒ So this won't work on its own.

function saySecret() {
  return 'šŸ¤«';
}
Enter fullscreen mode Exit fullscreen mode

āœ… You need to also call this in an exported function of the same file.

function sayHi(password) {
  if (password) {
    saySecret(); // šŸ‘ˆ Calling the non-export function
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, can you actually test it šŸ‘

// utils.test.js

import utilsRewire from './utils.js';

describe('saySecret', () => {
  it('returns shh emoji', () => {
    const saySecret = utilsRewire.__get__('saySecret');

    expect(saySecret()).toBe('šŸ¤«');
  });
});
Enter fullscreen mode Exit fullscreen mode

Warning! Vuex with Rewire

To my dismay, after I finally got rewire set up and successfully added testing for my non-export functions. When I serve up my Vue app, I got this error:

āŒ Uncaught Error: [vuex] actions should be function or object with "handler" function but "actions.default" in module "editor" is {}.

šŸ¤¦ā€ā™€ļø Like many developers, when one hits a roadblock, you shut the project and give up! NO! That's not the developer way -- you go to LinkedIn and starting looking for a new career šŸ˜– Again NO šŸ˜‚ Let's see what Google has to say!

Often, I'll tell junior developers to just Google it. But even googling is a skill that takes time to hone. And knowing what to search is important. So I'm going to share the terms I used:

  • (copy & paste the error)
  • Rewire not working with Vuex

Luckily on the second search, I found the solution! Turns out GitLab had the same problem and even posted a solution. Let me copy and paste their findings:

[Rewire] adds a default export to any module which does not already have one. This causes problems with our current pattern of using import * as getters from './getters.js' for Vuex resources because default will end up being an unexpected data type (object, not function). As a result we've had to add export default function() {} to each of our getters to ensure this doesn't cause Vuex to complain.

Excellent, not only did they explain the problem, they provided the solution šŸ‘

1. My Problematic Code

In my Vue app, I had the same pattern as GitLab. Not surprisingly, I work there so I just reference the same pattern from work šŸ˜…. This was my original setup:

// actions.js

export const someAction = () => {};
Enter fullscreen mode Exit fullscreen mode
// store/index.js

import * as actions from './actions';

export default {
  actions,
};
Enter fullscreen mode Exit fullscreen mode

2. The solution

Using the solution found from GitLab, all I had to do is add a default export like so:

// actions.js

export default function() {} // šŸ‘ˆ Add this!

export const someAction = () => {};
Enter fullscreen mode Exit fullscreen mode

Alternative solutions

Of course, I could avoid this default export by following a different pattern. On the official Vuex guide, they have a Shopping cart example you can reference. They have something like this:

// modules/cart.js

const actions = {
  someAction() {},
};

export default { // šŸ‘ˆ no problem cause there's the default!
  actions,
};
Enter fullscreen mode Exit fullscreen mode
// store/index.js

import cart from './modules/cart';

export default new Vuex.Store({
  modules: {
    cart,
  },
});
Enter fullscreen mode Exit fullscreen mode

Proficiency leads to Result!

Maybe down the road, I'll change it, But that's what I have now so I'll just leave it šŸ˜… In programming, I learned very early on, that there are always multiple solutions. There is often no best way, there's only the way that works for you šŸ‘

I like my current setup. And to be honest, I'm more experienced with this way (heads up, I work at GitLab). So for me, this is MY best way. And when you're working on a startup, proficiency is key. You don't want to spend your time spinning your wheels to learn something. It's all about the RESULT. Pick the tool you're more familiar and start producing šŸ’Ŗ

Beginner Friendly Resources

If you come from my Tidbit community, you will be familiar with my more beginner-friendly posts. However, with my journal series, some of the topics will be a bit more advance. As they are topics that I'm encountering while I'm building up my startup project. I'm learning so much from it so I just want to keep knowledge sharing. And to able to churn these post out quickly, I often won't be able to lay out the foundation -- so I apologize in advance to the more beginner folks šŸ˜“ But don't fret! We all once started as beginners, as long as we put in the work, we can all level up! šŸ§—ā€ā™€ļø

Here's what I'll do, I'll link up resources that might help you follow my entry a bit more. Thanks again for reading my journal and can't wait to share more!

Unit testing in JavaScript Part 1 - Why unit testing?

Jest Crash Course - Unit Testing in JavaScript

Resources


Thanks for reading ā¤
To find more code tidbits, please visit samanthaming.com

šŸŽØ Instagram šŸŒŸ Twitter šŸ‘©šŸ»ā€šŸ’» SamanthaMing.com
šŸ’– šŸ’Ŗ šŸ™… šŸš©
samanthaming
Samantha Ming

Posted on October 10, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related