Testing private functionality in JavaScript
9 JUL 2013
Start with this example, modified from http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/ by @philwalton
var myModule = (function() {
function foo() {
// private function `foo` inside closure
return "foo"
}
return {
bar: function() {
// use private foo
foo()
// public function `bar` returned from closure
return "bar"
}
}
}())
Testing function bar
is straightforward: the call to myModule()
should return the string 'bar' in this case.
But how do we test that the internal function foo
behaves correctly, given that it is hidden in a closure?
Take the tests for foo
and bar
in separate stages:
- Since foo is a dependency for
bar
, we need to testfoo
first. - Whether or not
foo
is to be private, we should testfoo
first as a public api. - If
foo
is to be private, then it shouldn't do too much. That will keep the tests short. - However, if
foo
does a lot, then it shouldn't be private. - Once you're satisfied with the
foo
tests, hide foo in the closure. - Then ask, how does
bar
usefoo
? justfoo(x)
orreturn foo(x)
? what gets passed to it? does it get modified? - And once you've answered that, you can let or make
bar
testfoo
for you. - Since you won't need the
foo
tests anymore, you can move assertions as you need them directly intobar
itself and modify as necessary. These would typeof or greater than assertions, or has a property, etc.
The point is, don't get hung up on private anything as something to be mocked. We don't need mocks so much as we good test+dev workflow and ergonomics. That prevents us from overthinking or seeking heroic solutions, and keeps away the anxiety and burnout ~ TDD is meant to do all that.
Overall, I agree with most of what you've said.
Where we may disagree is that I don't like the idea of testing a method and then removing the tests for it once the feature has been developed (assuming I've understood you right). You say:
I suppose this really comes down to what you see as the real value of unit tests.
For me, the primary reason I test my code is so that I can make changes in the future and be confident that I haven't broken anything. But if you're only testing private functions when you initially write them (by making them public and then, once tested, making them private), what do you do once you need to change that code or add new features? Do you make it public again as you're changing it so you can test it, and then make it private once those changes are done? To me, that seems like more work than the method I proposed.
I think I also disagree with these statements:
I think what you say is usually good advice, but perhaps not necessarily a general rule.
Here's a counter-example from a recent project I worked on:
I had a function whose job was to take a URL and a callback and, once complete, invoke the callback passing in the content found at that URL. If there was a request in-progress, the passed URL was added to a queue. Also, once a request was finished the result was cached so subsequent requests to the same URL would be instantaneous.
In the above case, the public function was pretty simple. Most of the logic was delegated to private functions that processed the queue, did the requesting, maintained the cache, etc. These functions offered no value on their own, so it didn't make sense to expose them publicly. Not to mention the fact that I didn't want the user manually altering the cache.
Anyway, it got to the point when I realized that simply testing the single public method was definitely not going to be sufficient. I could test that the
requestURL()
function returned a certain result, but how could I know for sure that the cache was working? How could I know that results were processed in the correct order? I found that I definitely wanted to write tests to make sure that multiple requests to the same URL wouldn't result in seperate AJAX requests, but in order to do that I needed to publicly expose those private functions and the private cache object.Anyway, that's just one example, but it's a real-life example (which, admittedly, none of the example in the post were).
I'm curious to know what you would have done in that situation.