loading...

Testing Non-Exported Functions in JavaScript

samanthaming profile image Samantha Ming Originally published at samanthaming.com ・5 min read

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 'πŸ‘‹';
}

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('πŸ‘‹');
  });
});

Non-export function

Now, what if the function is not exported?

function saySecret() {
  return '🀫';
}

Ah yikes, there is no way to test it! πŸ€·β€β™€οΈ

// utils.test.js

// ❌
import { saySecret } from './utils.js';

saySecret; // undefined

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

Step 2: Add to babel config

babel.config.js

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

Step 3: Using it

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

function saySecret() {
  return '🀫';
}

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('🀫');
  });
});

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 '🀫';
}

βœ… 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
  }
}

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('🀫');
  });
});

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 = () => {};
// store/index.js

import * as actions from './actions';

export default {
  actions,
};

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 = () => {};

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,
};
// store/index.js

import cart from './modules/cart';

export default new Vuex.Store({
  modules: {
    cart,
  },
});

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

Posted on by:

samanthaming profile

Samantha Ming

@samanthaming

Frontend Developer sharing weekly JS, HTML, CSS code tidbits πŸ”₯Discover them all on samanthaming.com πŸ’›

Discussion

pic
Editor guide
 

Friendly advice, try not to test implementations, rather do it with integrations. It's not worth when you do refractors or even renames and your tests start failing, although the logic and result of your program is still the same.

 

Fair pointβ€” thanks for sharing! πŸ’ͺ

 

Wouldn't these non-exported functions really be implementation details that don't really need testing? It doesn't (or shouldn't) matter how the internals work

(What Federico said!)

 

Just a bit curious. Why did you decide not to export the function?

 

Can you test the functions in a React functional component with this?

 

Unfortunately, I’m not super familiar with React, so I’m not sure πŸ˜“ ...but if you find out, please do share! I’m sure others would love to know as well 😊

 

Alright, I'll have a go at it and let you know of the outcome.