<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Timothy Ng</title>
    <description>The latest articles on DEV Community by Timothy Ng (@timorthi).</description>
    <link>https://dev.to/timorthi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F97313%2F705dea00-d7df-4efc-84a0-28fcc3f8fde4.jpeg</url>
      <title>DEV Community: Timothy Ng</title>
      <link>https://dev.to/timorthi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/timorthi"/>
    <language>en</language>
    <item>
      <title>50% Faster Testing with Mocha's Parallel Mode</title>
      <dc:creator>Timothy Ng</dc:creator>
      <pubDate>Sat, 19 Dec 2020 21:39:43 +0000</pubDate>
      <link>https://dev.to/timorthi/50-faster-testing-with-mocha-s-parallel-mode-5g6h</link>
      <guid>https://dev.to/timorthi/50-faster-testing-with-mocha-s-parallel-mode-5g6h</guid>
      <description>&lt;p&gt;Hey all! I originally published this post to &lt;a href="https://medium.com/leaselock-engineering/50-faster-testing-in-mochas-parallel-mode-a8eced30e823" rel="noopener noreferrer"&gt;LeaseLock's Engineering Blog&lt;/a&gt;, but I wanted to share it with the community here as well. In this post we &lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article references features of the Mocha testing library available from v8.2.0 onwards.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://mochajs.org/" rel="noopener noreferrer"&gt;Mocha&lt;/a&gt; and &lt;a href="https://www.chaijs.com/" rel="noopener noreferrer"&gt;Chai&lt;/a&gt;.  &lt;/p&gt;

&lt;h3&gt;
  
  
  A Good Problem to Have
&lt;/h3&gt;

&lt;p&gt;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.  &lt;/p&gt;

&lt;p&gt;From start to finish, our test suite takes &lt;strong&gt;about 2 minutes&lt;/strong&gt; to run, give or take a few seconds depending on hardware. It's not terrible, but it will quickly become a problem in our &lt;a href="https://www.businesswire.com/news/home/20200910005622/en/LeaseLock-Announces-500-Million-In-Leases-Insured" rel="noopener noreferrer"&gt;high-growth environment&lt;/a&gt; as we bring on more engineers and build out new features.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F800%2F1%2AJ-1MC3QGbIuwq4tb-yr-iA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F800%2F1%2AJ-1MC3QGbIuwq4tb-yr-iA.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  A relevant xkcd, except we'd be saying, "My tests are running." (&lt;a href="https://xkcd.com/303/" rel="noopener noreferrer"&gt;source&lt;/a&gt;)
&lt;/h6&gt;

&lt;p&gt;Acknowledging that our test suite was only going to get slower, we looked to Mocha's &lt;a href="https://github.com/mochajs/mocha/releases/tag/v8.0.0" rel="noopener noreferrer"&gt;v8 major release&lt;/a&gt;, which introduced parallel mode by utilizing &lt;a href="https://github.com/josdejong/workerpool" rel="noopener noreferrer"&gt;worker pools&lt;/a&gt;.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Just Add the &lt;code&gt;--parallel&lt;/code&gt; Flag
&lt;/h3&gt;

&lt;p&gt;If only it were that easy.  &lt;/p&gt;

&lt;p&gt;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.  &lt;/p&gt;

&lt;p&gt;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.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In parallel mode, we faced the challenge of making the aforementioned one-connection-at-a-time guarantee.&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F800%2F1%2A-XNuau6S_bSUA3LDq_Gfhw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F800%2F1%2A-XNuau6S_bSUA3LDq_Gfhw.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  What are the chances that multiple tests compete for the same database table at the same time? (Hint: Pretty likely.)
&lt;/h6&gt;

&lt;h3&gt;
  
  
  Concurrency Woes
&lt;/h3&gt;

&lt;p&gt;Core to arriving at our solution was understanding a few things about Mocha's parallel mode:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We can control the number of worker processes that Mocha spawns via the &lt;code&gt;--jobs flag. Without this flag, Mocha defaults to&lt;/code&gt;(num CPU cores-1)`.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each worker process is a Node &lt;a href="https://nodejs.org/api/child_process.html" rel="noopener noreferrer"&gt;child_process&lt;/a&gt;.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.)  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mocha's lifecycle hooks can be used to bootstrap our test environment.  We can use &lt;a href="https://mochajs.org/#global-fixtures" rel="noopener noreferrer"&gt;global fixtures&lt;/a&gt; to run setup and teardown exactly once. On the other hand, we can use &lt;a href="https://mochajs.org/#root-hook-plugins" rel="noopener noreferrer"&gt;root hook plugins&lt;/a&gt; to run &lt;code&gt;beforeAll&lt;/code&gt; before each test file. (Note: &lt;a href="https://mochajs.org/#available-root-hooks" rel="noopener noreferrer"&gt;the behavior of root hooks varies between parallel and serial modes&lt;/a&gt;, but for this article, we are only concerned with the parallel case.)  &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With these points in mind, we concluded that we could &lt;strong&gt;assign a dedicated database to each worker process&lt;/strong&gt;.  &lt;/p&gt;

&lt;p&gt;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.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F800%2F1%2AB6AwA8fdcLO_7LFxk34scw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F800%2F1%2AB6AwA8fdcLO_7LFxk34scw.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Since each worker runs tests serially, having a dedicated database for each worker removes the issue of concurrent access to the test database.
&lt;/h6&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How would we bootstrap database copies? Do we have to run our migrations on each database we spin up? &lt;/li&gt;
&lt;li&gt;How can we force the tests in a worker process to connect to the worker's dedicated database copy?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Brewing Method
&lt;/h3&gt;

&lt;p&gt;The Mocha library provides hooks into its lifecycle in the form of &lt;strong&gt;global fixtures&lt;/strong&gt; and &lt;strong&gt;root hook plugins&lt;/strong&gt;. We used these hooks to bootstrap our test databases in the appropriate stages of Mocha's lifecycle.  &lt;/p&gt;

&lt;p&gt;Using global fixtures, which is guaranteed to fire the &lt;code&gt;mochaGlobalSetup&lt;/code&gt; and &lt;code&gt;mochaGlobalTeardown&lt;/code&gt; 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.  &lt;/p&gt;

&lt;p&gt;Having the Postgres databases in a Docker container provides a nice ephemeral environment - perfect for ensuring a clean slate between test runs.  &lt;/p&gt;

&lt;p&gt;To save us from having to run our schema migrations every time we spin up a database for a worker process, we create a &lt;a href="https://www.postgresql.org/docs/current/manage-ag-templatedbs.html" rel="noopener noreferrer"&gt;template database&lt;/a&gt; so we can simply run &lt;code&gt;createdb --template my_template test_db_1&lt;/code&gt; to stand up a new database with the most up-to-date schema.  &lt;/p&gt;

&lt;p&gt;Our global fixtures file &lt;code&gt;--require&lt;/code&gt;d by Mocha looked roughly like:&lt;br&gt;
{% gist &lt;a href="https://gist.github.com/timorthi/13228a9ec10de4f9bbe486c0c864c7ba" rel="noopener noreferrer"&gt;https://gist.github.com/timorthi/13228a9ec10de4f9bbe486c0c864c7ba&lt;/a&gt; %}&lt;/p&gt;

&lt;p&gt;Great! &lt;strong&gt;Now that we have a database engine active while our tests are running, we had to actually create the databases for each worker process.&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;Our problems were two-fold:  &lt;/p&gt;

&lt;p&gt;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.  &lt;/p&gt;

&lt;p&gt;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.  &lt;/p&gt;

&lt;p&gt;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?  &lt;/p&gt;
&lt;h3&gt;
  
  
  A Blank Slate Each Time
&lt;/h3&gt;

&lt;p&gt;Mocha creates child processes with the &lt;a href="https://github.com/josdejong/workerpool" rel="noopener noreferrer"&gt;workerpool&lt;/a&gt; library which sits atop the &lt;code&gt;child_process&lt;/code&gt; module. At the end of the day, each new Mocha worker is just a fork() call.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each worker has no relation to each other nor its parent, so it can be manipulated freely without worrying about contaminating other environments.&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;process.env&lt;/code&gt; 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.  &lt;/p&gt;

&lt;p&gt;We opted to use the &lt;code&gt;pid&lt;/code&gt; as the unique identifier for each database copy and conjured up the following hooks file, also &lt;code&gt;--require&lt;/code&gt;d by Mocha:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h3&gt;
  
  
  Future Hours Saved
&lt;/h3&gt;

&lt;p&gt;With this setup, we are now able to run our full test suite in parallel.  &lt;/p&gt;

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

&lt;p&gt;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, &lt;strong&gt;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.&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F800%2F1%2Ax0IC5xGh15-1WDYkXuzqfQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F800%2F1%2Ax0IC5xGh15-1WDYkXuzqfQ.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  In a perfect world…
&lt;/h6&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;If you're interested in tackling problems like this with us, visit our &lt;a href="https://leaselock.com/careers/" rel="noopener noreferrer"&gt;careers page&lt;/a&gt; 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 &lt;strong&gt;&lt;a href="mailto:talent@leaselock.com"&gt;talent@leaselock.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>node</category>
      <category>testing</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
