2022-08-11
|~3 min read
|487 words
Imagine the following scenario: you have a javascript module that’s relatively complicated.
So complicated that you’ve broken it down into a main
function and then several smaller helper functions.
You only want the main
function to be exposed publicly.
All of the smaller functions are simply utilities.
They’re implementation details.
Callers shouldn’t be aware of them and so they shouldn’t be able to see them / call them.
This is all fairly standard as it goes, but now the question: how do you test the helper functions?
How do you guarantee that they work the way you’re expecting them to?
Do you only test main
?
And if not, how do you actually test the smaller functions?
One approach is to have a second exported module that wraps only the functions you want to test.
Let’s make this example more concrete.
// main.ts
const foo = () => {}
const bar = () => {}
const baz = () => {}
export const main = () => {
// do stuff...
foo()
// do more stuff...
bar()
// and even more stuff...
baz()
}
export const _testing = {
foo,
bar,
baz,
main,
}
What does this do?
Well, for one, it makes it clear that main
is available for other modules to import and use.
It also clearly designates that the functions we want to test are included in _testing
.
How do we actually use this?
In our main.test.ts
file, we import the _testing
module, not something else.
For example:
// main.test.ts
import { _testing as Main } from './main';
describe('Main', () => {
it('foo behaves as expected', () => {
expect(Main.foo()).to.eql(true);
})
})
This has been quite successful for me in keeping methods I want private, private.
However, there’s one caveat that I’ve come across: stubbing and mocking.
When I tried to use sinon
to stub a function for example that I was importing from _testing
it didn’t seem to attach to the proper function.
The workaround is that in that case I did need to make the method public and then stub based on the full module import.
For example:
// main.ts
const foo = () => {}
const bar = () => {}
export const baz = () => {}
export const main = () => {
// do stuff...
foo()
// do more stuff...
bar()
// and even more stuff...
baz()
}
export const _testing = {
foo,
bar,
baz,
main,
}
Now, baz
is part exported at the top level as well as within _testing
.
In our test, we would stub the top level method.
For example:
import { _testing as Main } from './main'
import * as MainActual from './main'
describe('main', ()=> {
it('foo behaves as expected', () => {
sinon.stub(MainActual, 'baz').returns('foo-bar-baz');
const bazSpy = sinon.spy(MainActual, 'baz');
expect(Main.foo()).to.eql(true);
})
})
Now we have both a stub and a spy for baz
using sinon
.
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!