Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Testing

Testing ESM with Mocha and testdouble.js

Dive into the world of ESM testing in Node.js with this detailed comparison of Mocha and Jest. Learn from my failures and find out what finally worked!
Pam-Marie Guzzo
|
October 28, 2021
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

I’m writing the blog I wish I’d seen about a month ago. Getting to the point of having unit tests with mocking and watching to work with ESM (ECMAScript Modules) was much harder than I at first envisioned.

This is a story of failure. There were times when I thought there would be no happy ending. So, for past me (and possibly future you), I’m going to explain what didn’t work and what eventually did.

A woman walks down a path in the woods, her arm outstretched behind her pulling someone else along by the hand
   Come with me... (photo by jay carpio)

Background

First I’m going to talk a little bit about what I was trying to test. The repo I was looking at is a node app with two parts: an Azure function written in CommonJS and an API written in ESM (complete with .mjs extensions). The CommonJS modules also used some of the ESM scripts through dynamic imports. This helped with reusability, but did mean you had to hunt a bit to know everything any given file was using.

I should note that a lot of my pain was caused by trying to use Jest. Why did I do this to myself, you ask?

  1.  Jest’s watch mode is great, and they have it working very nicely with ESM.
  2.  I’m a bit lazy, and Jest comes with tons of features, like matchers and coverage, that you have to add and configure separately in Mocha.
  3.  The other projects for this group are React with CommonJS. Jest is a natural fit for those types of projects. I was hoping to save the other devs some grief by giving them a consistent test framework.
  4.  I can be a bit too stubborn for my own good. Once I started down the Jest path I had to convince myself it was, in fact, not going to work.

For those who would like some more information on the differences between ESM and CommonJS, this blog from Redfin gives a really great overview. I found the article about halfway through my desperate search for answers. For the official documentation on the differences, you can find the Node documentation here.

The Jest path

“This blog is about Mocha, why are you going on about Jest?” you may be asking. Well, a couple of reasons. First, it’s important to talk about the times we failed, miserably, after many attempts. While it’s definitely fun to pretend I’m an all-knowing deity who never makes mistakes, that’s not the reality. I personally think part of the reason so many people get imposter syndrome is because there’s a cloud of secrecy around the things that don’t work. I want to help dispel that cloud.

The second reason is that I did a lot, and I mean a lot, of Googling around “how to test ESM with Jest” and found very little information about it. There was the occasional “just use x or y instead” post, but not a lot of detail on why. As mentioned previously, this blog is for past me more than anyone else.

Thirdly, maybe writing what went wrong will reveal to me or someone else what I missed. I think I went through all the permutations, but maybe I missed something. It’s very possible!

So here are some of the things I tried, in no particular order (because I have forgotten the order I did them in):

  • Tried mocking with Jest mocks, even though they 100% say in their documentation that mocking is not available yet for ESM.
  • Attempted to use Babel to transpile everything and then use Jest. This mostly didn’t work because of all the dynamic imports and the use of the .mjs extension. There was also pushback on testing transpiled code when we don’t use it in production.
  • Tried to use testdouble.js for mocking. I’ll go into how ESM mocking with testdouble.js works later when I talk about Mocha, but for Jest it didn’t work due to a conflict with how Jest handles loaders. My sadness did inspire a PR to prevent a problem I found attempting to use experimental loaders, so that’s good!

I definitely tried a bunch of other things I’ve since forgotten. I read many issues, blogs and StackOverflow questions about Jest, ESM, CommonJS, and mocking. I broke my PR and reverted again and again. Eventually, I gave up on my original plan.

A woman with her hands on her head, glasses placed to the side, staring in frustration at a MacBook
   Me after fighting with this for longer than I should have. (photo by Elisa Ventur)

The Mocha path

Once the Jest team confirmed I couldn’t do what I wanted, I scrapped most of the branch I had been working on. I kept the test cases I had created. But all the package changes, configuration files, and any documentation went straight into the trash. It was garbage and I had failed.

Now, in all my hunting I had seen a few references to using testdouble.js with Mocha, including at one point having this mocha section linked when I was looking up things about mocking ESM scripts.

It was actually embarrassing how easy the setup was once I had all the Mocha/Chai/testdouble packages installed. Here’s the package.json scripts section:

  "scripts": {
    "test": "mocha --loader=testdouble",
    "test:watch": "mocha --loader=testdouble test/unit --parallel --watch",
    "test:cov": "c8 mocha --loader=testdouble"
  }

If you’re familiar with common Mocha setups, you might notice a couple of oddities. Despite what the Mocha docs say, watch mode can work if you run in parallel. However, I wouldn’t recommend this with large test suites as it will run all the tests on every save. This is why we limited the watch mode to just unit tests. You might also note I’m using c8 for coverage instead of nyc (the coverage package formally known as Istanbul). This is actually recommended by the nyc team, although they have an experimental loader for anyone who wants to try that out instead.

The settings for mocha only required a little bit of customization, mostly because of the mix of CommonJS and ESM. The main addition that mattered for the .mocharc.json was the extension setting:

{
    "extension": ["js", "cjs", "mjs"],
    "ui": "bdd",
    "require": "mocha.setup.mjs"
}

Finally, we added a setup file to give us some globals (because I hate adding Mocha and Chai to every test file) and reset any doubles we add:

import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

chai.use(chaiAsPromised);
global.expect = chai.expect;

import * as td from 'testdouble';
global.td = td;

export const mochaHooks = {
  afterEach() {
    td.reset();
  }
};

Once that was all done, writing the tests was a cinch!

describe('moduleToTest', () => {
  let moduleToMock;
  let moduleToTest;

  beforeEach(async () => {
    moduleToMock = await td.replaceEsm('./fake/moduleToMock.mjs');
    moduleToTest = await import('./fake/moduleToTest.mjs');
  });

  it('works and calls the mock', async () => {
    td.when(moduleToMock.prototype.doAThing(td.matchers.anything())).thenResolve();
    await expect(moduleToTest.realFunction()).to.eventually.be.fulfilled;
  });
});

The road goes ever ever on

Software development to me always feels like a long, occasionally arduous, journey. It often feels as though there’s an infinite amount of learning to do. The problems of today will not be the problems of tomorrow. We’ll never have all the solutions, and sometimes finding alternatives is frustrating.

All we can do is forge ahead, and leave a trail.

Related Insights

🔗
Happier TDD with testdouble.js
🔗
testdouble.js 2.0: your new go-to for JavaScript testing
🔗
testdouble.js vs. sinon.js: Which is better for JavaScript testing?

Explore our insights

See all insights
Developers
Developers
Developers
You’re holding it wrong! The double loop model for agentic coding

Joé Dupuis has noticed an influx of videos and blog posts about the "correct" way of working with AI agents. Joé thinks most of it is bad advice, and has a better approach he wants to show you.

by
Joé Dupuis
Leadership
Leadership
Leadership
Don't play it safe: Improve your continuous discovery process to reduce risk

We often front-load discovery to feel confident before building—but that’s not real agility. This post explores how continuous learning reduces risk better than perfect plans ever could.

by
Doc Norton
Leadership
Leadership
Leadership
How an early-stage startup engineering team improved the bottom line fast

A fast-growing startup was burning cash faster than it could scale. Here’s how smart engineering decisions helped them improve the bottom line.

by
Jonathon Baugh
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.