On this page

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:

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 mocha and @types/mocha dependencies from package.json.
  • node:test does not support Mocha's retry option, so tests relying on it need to be handled separately.