50% Faster Testing with Mocha's Parallel Mode
Tags: node, testing, webdev, javascript
Published: 2020-12-19 21:39:43

Hey all! I originally published this post to [LeaseLock's Engineering Blog](https://medium.com/leaselock-engineering/50-faster-testing-in-mochas-parallel-mode-a8eced30e823), but I wanted to share it with the community here as well. In this post we 

---

_This article references features of the Mocha testing library available from v8.2.0 onwards._

At LeaseLock, we take pride in our codebase's ~93% test coverage. Despite being a small team, we rarely introduce new functionality without accompanying tests - this rule has served us well by keeping us away from silly mistakes. At the time of writing, we have just over 3,000 test cases in our test suite powered by [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/).  


### A Good Problem to Have
While most of our tests are rapid-fire unit tests, there are a significant number of integration and end-to-end tests that hit our test database. As one would expect, these I/O bound tests significantly slow down the overall runtime of our tests.  

From start to finish, our test suite takes **about 2 minutes** to run, give or take a few seconds depending on hardware. It's not terrible, but it will quickly become a problem in our [high-growth environment](https://www.businesswire.com/news/home/20200910005622/en/LeaseLock-Announces-500-Million-In-Leases-Insured) as we bring on more engineers and build out new features.  

![](https://cdn-images-1.medium.com/max/800/1*J-1MC3QGbIuwq4tb-yr-iA.png)
###### A relevant xkcd, except we'd be saying, "My tests are running." ([source](https://xkcd.com/303/))  

Acknowledging that our test suite was only going to get slower, we looked to Mocha's [v8 major release](https://github.com/mochajs/mocha/releases/tag/v8.0.0), which introduced parallel mode by utilizing [worker pools](https://github.com/josdejong/workerpool).  


### Just Add the `--parallel` Flag
If only it were that easy.  

By running our tests serially, we were able to make the nice assumption that exactly one test case was accessing the database at a given moment.  

With multiple worker processes chipping away at our test suite, contention between two or more test cases for the same database table is bound to happen.  

**In parallel mode, we faced the challenge of making the aforementioned one-connection-at-a-time guarantee.**  

![](https://cdn-images-1.medium.com/max/800/1*-XNuau6S_bSUA3LDq_Gfhw.png)
######What are the chances that multiple tests compete for the same database table at the same time? (Hint: Pretty likely.) 
 

### Concurrency Woes
Core to arriving at our solution was understanding a few things about Mocha's parallel mode:
1. We can control the number of worker processes that Mocha spawns via the `--jobs flag. Without this flag, Mocha defaults to `(num CPU cores-1)`.  

2. Each worker process is a Node [child_process](https://nodejs.org/api/child_process.html).  

3. Workers run test suites file-by-file, but the order in which files get processed - and by which worker - is arbitrary. (In other words, each test file must run successfully in isolation.)  

4. Mocha's lifecycle hooks can be used to bootstrap our test environment.  We can use [global fixtures](https://mochajs.org/#global-fixtures) to run setup and teardown exactly once. On the other hand, we can use [root hook plugins](https://mochajs.org/#root-hook-plugins) to run `beforeAll` before each test file. (Note: [the behavior of root hooks varies between parallel and serial modes](https://mochajs.org/#available-root-hooks), but for this article, we are only concerned with the parallel case.)  


With these points in mind, we concluded that we could **assign a dedicated database to each worker process**.  

The idea was simple: for each worker that Mocha spawns, we'd want to create a copy of the test database that only that worker should connect to. With this design, we'd prevent contention between multiple worker processes by eliminating concurrent access to the same test database.  

![](https://cdn-images-1.medium.com/max/800/1*B6AwA8fdcLO_7LFxk34scw.png)
###### Since each worker runs tests serially, having a dedicated database for each worker removes the issue of concurrent access to the test database.  

From here, all we had to do was find the right places to bootstrap the databases. A few questions stood out when we first approached this solution:
- How would we bootstrap database copies? Do we have to run our migrations on each database we spin up? 
- How can we force the tests in a worker process to connect to the worker's dedicated database copy?

### The Brewing Method
The Mocha library provides hooks into its lifecycle in the form of **global fixtures** and **root hook plugins**. We used these hooks to bootstrap our test databases in the appropriate stages of Mocha's lifecycle.  

Using global fixtures, which is guaranteed to fire the `mochaGlobalSetup` and `mochaGlobalTeardown` functions exactly once per run, we perform two things: 1) spin up a Docker container of the Postgres engine, and 2) create a template database that can be copied for each worker process.  

Having the Postgres databases in a Docker container provides a nice ephemeral environment - perfect for ensuring a clean slate between test runs.  

To save us from having to run our schema migrations every time we spin up a database for a worker process, we create a [template database](https://www.postgresql.org/docs/current/manage-ag-templatedbs.html) so we can simply run `createdb --template my_template test_db_1` to stand up a new database with the most up-to-date schema.  

Our global fixtures file `--require`d by Mocha looked roughly like:
{% gist https://gist.github.com/timorthi/13228a9ec10de4f9bbe486c0c864c7ba %}


Great! **Now that we have a database engine active while our tests are running, we had to actually create the databases for each worker process.**  

Our problems were two-fold:  

First, our codebase relies on environment variables to fetch database connections. We needed to ensure that the worker process started up with the correct environment variables to connect to its dedicated database.  

Second, there aren't any hooks for when a worker process is spawned by Mocha. We needed a way to create the worker's dedicated database exactly once per worker, but had no Mocha hook to do so.  

These issues are closely intertwined. If we can't hook into the worker-spawning process, how can we provide the worker processes with the correct environment, or spin up its database efficiently?  

### A Blank Slate Each Time
Mocha creates child processes with the [workerpool](https://github.com/josdejong/workerpool) library which sits atop the `child_process` module. At the end of the day, each new Mocha worker is just a fork() call.  

**Each worker has no relation to each other nor its parent, so it can be manipulated freely without worrying about contaminating other environments.**  

A child process's memory space is isolated from sibling and parent Node processes. This takes care of both the aforementioned problems. First, regarding the environment variables, we can safely edit the `process.env` property within a worker. Second, we can manipulate the global state within our code to maintain a flag on whether a database for a given worker process had already been created.  

We opted to use the `pid` as the unique identifier for each database copy and conjured up the following hooks file, also `--require`d by Mocha:
{% gist https://gist.github.com/timorthi/8905ee573a8eb301f55b36390a14fa0f %}


### Future Hours Saved
With this setup, we are now able to run our full test suite in parallel.  

With some tuning of the number of workers - 4 seems to be a good number for our team's hardware - **we've seen anywhere from a 30% to 60% improvement in overall runtime, saving us precious minutes daily in our development loop**. An added benefit is that our CI build times are down too!  

In addition to the initial gains in performance, we're excited to see what happens as we increase the number of test suites in our codebase. In theory, **if we run Mocha with a parallelism of N, it would take N new test files for the runtime to increase as much as 1 new test file would in serial mode.**  

![](https://cdn-images-1.medium.com/max/800/1*x0IC5xGh15-1WDYkXuzqfQ.png)
###### In a perfect world…  

We've kept things simple here at LeaseLock, so the only data store that our tests interact with is the Postgres database. As the codebase grows, we'll inevitably add more data stores or external services that need to be tested end-to-end. When that happens, we'll be sure to take our learnings from this iteration of test parallelization and apply them as needed.


---

If you're interested in tackling problems like this with us, visit our [careers page](https://leaselock.com/careers/) for information about available roles. If you don't see the role you're looking for, you can also reach out to us directly at **talent@leaselock.com**.