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 addexport 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
- GitHub: Jest test on function that's not exported
- Unit Testing Private, Non-exported Functions with Rewire
- Stack Overflow: Using babel-plugin-rewire to test private non-referenced functions
- GitLab: Remove babel-plugin-rewire
Thanks for reading β€
To find more code tidbits, please visit samanthaming.com
π¨ Instagram | π Twitter | π©π»βπ» SamanthaMing.com |
Top comments (7)
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.