◆ package.json を作らずパッケージ化してないちょっとしたスクリプトが多い
◆ npm ライブラリのテストツールを入れるなら package.json を作ってパッケージ化が必要
◆ それらをテストのためだけにパッケージ化したくない
  ◆ ちょっとしたスクリプトに対してかなり重たいテストツールをインストールしたくない
◆ ビルトイン機能なのでパッケージ化不要で簡単に使える

少し前に Node.js ではテストを実行する機能が追加されて LTS では 16 と 18 で使えます
Node.js のテスト機能は npm パッケージに色々あり Node.js の組み込みに追加されても別にどうでもいいかなとスルーでした

しかし考えてみると結構良いものなのかもしれません

パッケージ化したくないとき

package.json を作ってパッケージ化している場所ならテスト用のパッケージを追加するだけですが パッケージ化していないちょっとしたスクリプトというものも結構あります
1 つか 2 つの .js ファイルだけのところを いちいちパッケージ化はしたくないです
ちょっとしたスクリプトならわざわざテストコードを書くまでも無いことも多いのですが ロジックが少し複雑なものだとテストツールを使ったテストをしておきたいケースもあります
使い方の説明や 入力に対してどんな出力が起きるのかを文章で書くよりテストコードを書いておいてそれを見る方が楽ってこともありますし

そういうときに Node.js に組み込みの機能なら パッケージ化したりせず簡単に使えます

簡単に使ってみました
他でよく見るのに似たフォーマットなので簡単でした

ありがちなものですが add.js が足し算する関数をエクスポートしてる場合はこういう感じです

const assert = require("assert")
const { test, it, describe } = require("node:test")

const add = require("./add.js")

test("1 + 1", (t) => {
assert.strictEqual(add(1, 1), 2)
})

出力の方はあんまり見やすくないものでした

root@694e65721638:/tmp/nodetest1# node --test add.test.js
TAP version 13
# Subtest: /tmp/nodetest1/add.test.js
ok 1 - /tmp/nodetest1/add.test.js
---
duration_ms: 64.5171
...
1..1
# tests 1
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 69.1822

実際には ちょっとしたスクリプトだと こういうライブラリ的な部分に分けず そのままトップレベルでなにかの処理をすることが多いです
例えば 引数をファイルに追記するスクリプトがあったとします

[append-message.js]
const fs = require("fs")

const [message] = process.argv.slice(2)

if (message) {
fs.appendFileSync("./output", message)
}

この場合だとこういう感じでしょうか

const assert = require("assert")
const { test, it, describe } = require("node:test")
const fs = require("fs")

test("append message", (t) => {
fs.writeFileSync("./output", "foo")

const argv = process.argv
process.argv = ["", "", "message1"]

require("./append-message.js")

process.argv = argv

assert.strictEqual(fs.readFileSync("./output").toString(), "foomessage1")
})

あまりわかりやすくないですね
それに require を使うと 1 回しか実行できません
別パターンで実行したいこともありえますし グローバル変数を色々書き換えるスクリプトの場合もありえます
別プロセスで実行するほうがいいかもしれません

機能は少なめ

そんな感じで基本的なテスト機能があるので パッケージ化したくないときや軽く使いたい場合には良さそうです
ただ パッケージ化してる部分になると 組み込み機能は機能が少なめなので jest などを入れたほうが良いと思います

個人的には モジュールのモック機能は必須レベルに重要なのですが 現状ではありません
Node.js 19 ではモック機能はあるのですが 関数やメソッド用です
require を置き換えて 本来のモジュールを読み込まずにテスト用モジュールに置き換えたいのですが そういうことはできないです
Node.js には yarn pnp などで使われる loader のカスタマイズ機能はあるらしいので その辺をうまく使って実現してくれるといいのですけど

ドキュメントと一致しない

まだ experimental のステータスなので仕方ないのでしょうがドキュメントと一致しない挙動がありました

https://nodejs.org/docs/latest-v18.x/api/test.html#describename-options-fn

ここによれば describe を使うとコールバックの最初に SuiteContext を受け取るようです
test だと TestContext が受け取れるところが SuiteContext になっているようです

実際に試してみます

const { test, it, describe } = require("node:test")

test("test", test_ctx => {
console.log(test_ctx)
})

describe("describe", suite_ctx => {
console.log(suite_ctx)
})

結果は

TestContext {}
[]

となりました
SuiteContext ではなく空の配列でした

ちなみに現状だと 「node --test」 でテストモードとして実行すると console.log の中身はコンソールに表示されません
console.error でエラー出力に送ったり process.stdout.write で直接書き込んでも同じです
サブプロセスで実行して出力は捨ててそうです

なので 「--test」 をつけずに 「node test.js」 のように普通に実行するか util.inpsect でフォーマットしてファイルに書き込みするかしないと結果が見れません

原因

SuiteContext ではなく 空のオブジェクトでもなく なぜか配列になっているのが気になったのでソースをみてみました

https://github.com/nodejs/node/blob/v18.12.1/lib/internal/test_runner/test.js#L705

PromiseResolve(this.runInAsyncScope(this.fn, ctx, args)),

で describe に渡した関数 this.fn が実行されています
runInAsyncScope は 3 つ目以降の引数を ...args で受け取り ReflectApply で展開します
なので runInAsyncScope に渡すところで ... で展開しておく必要があるはずです
それがないので配列として受け取っているようです

test の方だとちゃんと TestContext が受け取れていますが test の方では

const ret = ReflectApply(this.runInAsyncScope, this, runArgs);

となっていて runInAsyncScope を直接呼び出さず ReflectApply 経由で呼び出しているからのようです
最新版だと修正済みかなと 19 の方を見ましたが 今のところ変わりないようです

それより気になるのは配列が空だったんですよね
渡し方がおかしくても SuiteContext が配列の 1 つ目の要素には入っていてもよさそうなのですけど

getRunArgs を見ると args は [] で固定なので ドキュメントのほうが間違ってるのかもしれません
this として受け取れる ctx は

return { ctx: { signal: this.signal, name: this.name }, args: [] };

なので第一引数ではなくて this で受け取れるが正しいのかもです
ただ すべてアロー関数で済ませたいので this が必須になるのはできればやめてほしいのですけどね

Test の方の run メソッドだと this.fn と ctx を args に unshift で追加してるので同じものを期待して args を空にしてるだけとも考えられます

なんにせよ バグなのかよくわからないものもある状況なので 本格的に使うなら experimental ステータスを抜けるのを待ったほうがいいのかもしれませんね

追記:直ってた

その後 Node.js 20 でテストモジュールが Stable になったので Node.js 20.5 で試してみたら ちゃんと SuiteContext を受け取れるよう修正されていました

const { test, describe } = require("node:test")

test("test", test_ctx => {
console.log(2, test_ctx)
})

describe("describe", suite_ctx => {
console.log(1, suite_ctx)
})
1 SuiteContext {}
2 TestContext {}

console.log で表示される順も変わってますね