How to Unit Test a Nested Callback Function

A simple testing pattern for a scenario that can be a little tricky

Michael Jacobson
The Startup
3 min readAug 15, 2020

--

Man looking at computer frustrated or confused
Image by Oladimeji Ajegbile on Pexels

Writing unit tests can be fun but also tricky, especially if you’re testing code that may not have been written with unit testing in mind.

One scenario that I keep encountering in various ways — my own unit testing tasks, teammates at work asking “How do I test this?”, this StackOverflow question — is how to unit test a module or function that has business logic contained within a callback function being invoked by a nested entity that needs to be mocked for the test.

The Scenario

Here’s an example of app code I’m talking about (Node app code, in this case) that we’d like to write unit tests for:

(This is a simplified example — I’ve seen callback functions that go for miles.)

The unit we’re testing here is our customworklist module, which is wrapping the Node fs module in a Promise. We want to add some unit tests for the business logic contained in the callback function being passed to fs.readFile.

However, that callback gets invoked by fs after its file read is complete, and since we’ll be mocking fs for our unit tests, how do we test that code?

A Different View

I think what can help is to remember that that passed-in callback is just another argument being passed to the readFile method, like a string or a number. So if, in your mind (I’m not suggesting you modify the app code), you replace that noisy inline function with a simple function reference, it can help clarify what’s going on:

So now you can see that if we mock fs.readFile, our mock will receive the argumentsfilepath and callback, so you can visualize the app code like this:

The Final Test

Since our mock readFile method is receiving that callback as a parameter, in our mock we can invoke the callback with whatever err and data arguments we choose in order to test our different scenarios.

For example, here’s what the test for a file read error would look like (this is using Mocha, Chai, and rewire, but the approach is not library-specific):

When worklist() is called, the Promise contructor is called, which immediately invokesfs.readFile, which is actually our mock’s readFile method.

In our mock, manually invoking the callback with our desired arguments for this particular test exercises the business logic and causes the Promise to reject, which we expect inside the error handler on line 13.

An Alternative using Sinon.JS callsArgWith

If you’re using Sinon.JS for your spies, you can even simplify this a bit by using the stub.callsArgWith method.

With that handy method, you can create a spy for the fsMock.readFile method that will invoke the callback for you. So your test would look like this:

You just pass into callsArgWith the callback argument index (1 in our case since it’s readFile's 2nd argument) followed by the arguments that should get passed into the callback—in our case, errorResponse and mockFileData.

This saves you the step of invoking the callback yourself. Thanks, Sinon! 👍

Parting Thoughts

Writing unit tests can be challenging sometimes, especially for legacy code written by developers who were clearly unburdened by the question “How will I unit test this?”

I generally find the challenging cases enjoyable, though, because it becomes like solving a puzzle. And when you eventually figure out a solution, it’s much more rewarding than just another straightforward test you’ve written hundreds of times before.

--

--

Michael Jacobson
The Startup

Frontend Developer working with Angular for 10+ years. I love solving problems and building cool stuff. I sweat the details because…I love the details.