Mocha test cases with Node.js, TypeScript, ESM Modules and Chai
Being relatively new to Node.js and TypeScript and especially writing unit tests in this environment, I found it rather cumbersome to get started with writing Mocha cases with TypeScript, especially using the ECMAScript modules (ESM) packaging. It took me hours of detective work to find pieces of functioning examples, documentation and other clues on how to put all this together, so maybe these notes can help someone out.
First you setup the project and install the core packages:
% node -v
v20.11.0
yarn init
yarn add -D typescript mocha @types/mocha chai @types/chai
You’ll need a basic Mocha configuration file to instruct Mocha to look for the ts
file extension, the other options shown here are not strictly speaking required but do make the intent more obvious.
% cat .mocharc.json
{
"extension": ["ts"],
"reporter": "spec",
"spec": ["test/**/*.ts"]
}
Adding a dummy test case
Create a test file with a dummy test case to be executed, saving the file as test/example.spec.ts
.
import "mocha"
import {expect} from "chai"
describe('test suite', () => {
it('test case', function() {
const num : number = 5
expect(num).to.be.above(4)
})
})
If you run Mocha now, you’ll get an error message from Node.js not understanding the TypeScript .ts
file type:
% yarn mocha
yarn run v1.22.22
$ /Users/tonttu/mocha1/node_modules/.bin/mocha
Exception during run: TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/tonttu/mocha1/test/example.spec.ts
We’ll solve this by installing the ts-node package:
% yarn add -D ts-node
We also need to instruct Mocha to define the ts-node module loader by adding this to the configuration file .mocharc.json
:
"loader": "ts-node/esm"
Running yarn mocha
now will give you the error:
Exception during run: ReferenceError: exports is not defined in ES module scope
This implies that the TypeScript transpiling is producing CommonJS code and you’ll need to initialize your TypeScript configuration file by running yarn tsc --init
and changing the compilerOptions.module
setting to nodenext
to produce ESM module code. The defaults will be otherwise acceptable for this test setup.
Once these settings have been made, you should be able to execute the simple example test case:
% yarn mocha
yarn run v1.22.22
$ /Users/tonttu/mocha1/node_modules/.bin/mocha
(node:68813) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()`:
--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("ts-node/esm", pathToFileURL("./"));'
(Use `node --trace-warnings ...` to show where the warning was created)
test suite
✔ test case
1 passing (1ms)
✨ Done in 0.59s.
But what is that horrible looking ExperimentalWarning?
The warning comes from Mocha supplying (via ts-node) the Node argument--loader
in order to get the TypeScript transpiler support injected into the supported file types.
There is a pending GitHub pull request for the ts-node (10.9.2) project to start using the recommended register() function for registering the file handler. This approach complies with the changing Node.js requirements and allows Mocha to be started with the--import ts-node/esm-register
option to load the handler registration code without loud complaints.
If you clone and build the PR locally and configure your project to use it, the ugly complaint will go away and you can focus on your test outputs. Of course, the down side will be having to work through the ts-node updates, but hopefully the PR will be accepted soon enough.
Please note, if you are using a newer yarn (e.g. 4.4.1), you can replace the cloning and installing parts by simply running
yarn add -D https://github.com/jlenon7/ts-node.git#ts-node/esm-register
. The older 1.x yarn fails to deploy the bin scripts when installing from a Git URL.
yarn remove ts-node
git clone https://github.com/jlenon7/ts-node.git --branch ts-node/esm-register
pushd ts-node ; yarn install && yarn build ; popd
yarn add -D link:./ts-node
To start using the register patch, change your Mocha configuration file to use the require
option instead of the loader
option and point to to the new esm-register
module:
{
"extension": ["ts"],
"reporter": "spec",
"spec": ["test/**/*.ts"],
"require": "ts-node/esm-register"
}
Alternatively to defining the require
option in Mocha configuration, you can start the Node.js binary directly and provide the--import
argument yourself. I actually prefer this approach as it brings explicit visibility to the arguments passed to the runtime.
% node ./node_modules/.bin/mocha --import=ts-node/esm-register
% # (or start via yarn)
% yarn mocha --import=ts-node/esm-register
test suite
✔ test case
1 passing (1ms)
Finally, here are the current package versions at the time of writing.
% cat package.json
{
"name": "mocha1",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"type": "module",
"devDependencies": {
"@types/chai": "^4.3.17",
"@types/mocha": "^10.0.7",
"chai": "^5.1.1",
"mocha": "^10.7.3",
"ts-node": "link:./ts-node",
"typescript": "^5.5.4"
}
}
Still, the setup is not perfect and future issues might come up, but at least the output is tidy and test cases are performing as expected. With an additional failure test case added, the output should look like this:
I’ve continued my experiments with Chai and Chai-HTTP and plan to show some further examples in future posts, the TypeScript system being as nice as it is, has brought up some extra learning-curve there as well.
Let me know if you have had similar experience or have thoughts on this!