Unit-testing a child process in a Node.js\Typescript app

Tzafrir Ben Ami
4 min readJan 6, 2023
Image by CopyrightFreePictures from Pixabay

Recently I was involved in building a Node.js app written in Typescript that integrates with Git repositories. The app executes Git commands by calling the Git command-line tool in a shell-like environment using Node.js child_process module.

Node.js child_processmodule provides the ability to spin a child process and execute external programs without blocking the Node.js event loop. There are plenty of useful articles and code examples of using this module, but we were lucking knowledge on unit-testing¹ this code.

Interacts with a child process can be challenging — the process can crash or hang, communication between processes can fail, etc — which makes unit testing even more challenging and sometimes neglected.

Should we even unit-test it?

In theory, theory and practice are the same. In practice, they are not
(attribute to many people including Albert Einstein, Yogi Berra and others)

Ideally, we would like our unit tests to cover 100% of our code but we also need to be pragmatic: if testing a specific function or a component is too complicated or requires too much effort (like massive refactoring to make it testable), we might decide to exclude it.

True, this is not perfect but in some cases a “good enough” solution when coding. Having solid integration and system tests can somewhat compensate for the absence of unit tests, also in general they don’t carry the same functionality so having one doesn’t mean you can skip the other.

Going back to our use case, we knew that we want to validate that our code is correct with unit tests. Not to mention we had no integration\system tests during the early development phase.

How can we unit-test it?

The code to spin a child process² and execute a command might look something like the code below:

download source code from: https://github.com/tzafrirben/child_process

The execCommand function spawn a new child process (line 5) with a command to execute (Git command in our case) and buffer its output from the standard stdout/stderr (lines 10–18) until the process has ended (line 20).

Note that this code can be refactored to make it more testable, and we’ll explore it later on, but first let’s see how can we unit-test it without making any changes.

Write unit test using mocks

Mocks enable us to isolate and replace dependencies in our code that we don’t control with dependencies that we do control. In ourexecCommand function, for example, we do not control the child process instance returned from spawn (line 8).

The spawn function returns ChildProcess instance which implements the Node.js EventEmitter API, allowing the parent process to listen to events that occur during the child process life cycle. This also allows us to create a fake child process, which is basically an instance of EventEmitter, and use it in our testing.

Let’s look at simplified code to create a fake child process object:

But this is still not enough since we also need to make sure that the fake child process (the one we control) will be returned when spawning a new child process. For this we can use Jest mocks³:

When starting the test we create a fake child process (line 5) and then stubbing spawn function (line 8), replacing its returned value with our fake child process. Now we can trigger the data events on the fake process (lines 16–17) and eventually “end” it using the close event (line 20).

This can work, but as mentioned above, we can refactor our code to make it more testable. Let’s look at this option now.

Write unit test using dependency injection

Our code can be more testable by following the dependency injection pattern. In a nutshell, dependency injection means that a function\object will get the resources it requires as input parameters instead of constructing it.

To implement this pattern, let’s refactor execCommand function to receive the spawn child process as a parameter instead of creating a new instance on its own:

The immediate benefit of this change is that we don’t need to stub spawn in our unit tests and instead, we can send our fake child process as a parameter to the execCommand function:

Here we also start the test by creating a fake child process (line 5), but instead of stubbing the spawn function we just send it to the execCommand function. The rest of the test is basically the same.

Quick recap

Testing all the different scenarios/exceptions/errors that can emerge when working with a child process is almost impossible, but using the right tools and methodology we were able to validate that our code meets its functional requirements.

¹ few useful posts I’ve googled recently when writing this post:

² The child_process module provides multiple functions, but all are implemented on top of spwan (or the equivalent spawnSyn), so this is the function I’ve used in this post.

³ there are a few testing frameworks and mocking libraries in the JavaScript ecosystem, in this post I’ve used Jest but you can use other testing frameworks as well.

--

--

a "Full stack technology leader", trying to solve complex business problems with technology - mainly on the Web and large-scale systems (but not just)