Migrates Mocha 8.x test suites to the built-in Node.js test runner (node:test, available in Node.js 22.x and 24.x). It adds the required node:test imports for the globals a file uses (describe, it, before, after, beforeEach, afterEach), converts done callbacks to the (t, done) signature, rewrites this.skip() to t.skip() and this.timeout(N) to { timeout: N } options, and preserves the original function style (it never converts between function() and arrow functions). Both CommonJS and ESM files are supported, and the mocha and @types/mocha dependencies are removed from package.json afterwards.
Run this codemod with:
npx codemod @nodejs/mocha-to-node-test-runner
Global describe/it usage keeps working once the matching require('node:test') is inserted; modifiers like describe.skip are already compatible.
const assert = require('assert'); +const { describe, it } = require('node:test'); describe('Array', function() { describe.skip('#indexOf()', function() { it('should return -1 when the value is not present', function() { const arr = [1, 2, 3]; assert.strictEqual(arr.indexOf(4), -1); }); }); });
In ESM files an import statement is inserted instead.
import assert from 'assert'; +import { describe, it } from 'node:test'; describe('Array', function() { describe.skip('#indexOf()', function() { it('should return -1 when the value is not present', function() {
Only the hooks actually used in the file are added to the import list.
const assert = require('assert'); const fs = require('fs'); +const { describe, before, after, it } = require('node:test'); describe('File System', () => { before(function() { fs.writeFileSync('test.txt', 'Hello, World!'); }); after(() => { fs.unlinkSync('test.txt'); });
Mocha passes done as the first callback argument; node:test passes the test context first, so (done) becomes (t, done).
const assert = require('assert'); +const { describe, it } = require('node:test'); describe('Callback Test', function() { - it('should call done when complete', function(done) { + it('should call done when complete', function(t, done) { setTimeout(() => { assert.strictEqual(1 + 1, 2); done(); }, 100); }); });
this.skip() becomes t.skip(), with the test context parameter t added to the callback signature as needed.
const assert = require('assert'); +const { describe, it } = require('node:test'); describe('Skipped Test', () => { it.skip('should not run this test', () => { assert.strictEqual(1 + 1, 3); }); - it('should also be skipped', () => { - this.skip(); + it('should also be skipped', (t) => { + t.skip(); assert.strictEqual(1 + 1, 3); }); - it('should also be skipped 2', (done) => { - this.skip(); + it('should also be skipped 2', (t, done) => { + t.skip(); assert.strictEqual(1 + 1, 3); }); });
this.timeout(N) calls on suites and tests move into the { timeout: N } options argument.
const assert = require('assert'); +const { describe, it } = require('node:test'); -describe('Timeout Test', function() { - this.timeout(500); +describe('Timeout Test', { timeout: 500 }, function() { - it('should complete within 100ms', (done) => { - this.timeout(100); + it('should complete within 100ms', { timeout: 100 }, (t, done) => { setTimeout(done, 500); // This will fail }); - it('should complete within 200ms', function(done) { - this.timeout(200); + it('should complete within 200ms', { timeout: 200 }, function(t, done) { setTimeout(done, 100); // This will pass }); });
- After the transformation, the codemod detects your package manager and removes the
mochaand@types/mochadependencies frompackage.json.
node:testdoes not support Mocha'sretryoption, so tests relying on it need to be handled separately.