Node.js の assert の小話

Node.js Advent Calendar の4日目の記事 です。

 Node.js の assert は結構歴史が深いです。あまり直接使ってる人は少ないかもしれません。使うとしたら test で使ったりするケースでしょうか。 それも最近は jest に生えてる便利ライブラリを使うほうが多いのかもしれないですね。 unassert なんかで開発中に埋め込んでいるケースもあるかもしれません。このようにたまに使うこともあると思うので、覚えておくと良いでしょう。

assert には 4 年ほど前から strict assertion mode というのが追加されています。

nodejs.org

今日はそんな小話を。

require("assert") は直接使ってはいけなかった。

もう昔の話ですが、 require("assert") が deprecated になっていた時期がありました。知らない人も多いんじゃないかと思います。 なんで deprecated だったかというと、 一部の関数が意図しない動きをしていたからです。

const assert = require("assert");
assert.deepEqual(/a/gi, new Date()); // 例外が上がらない (v9.0 時点での話。v10.0 以降だと例外が上がる)
assert.deepEqual('+00000000', false); // 例外が上がらない (最新でも例外が上がらない)

deepEqual は中身のプロパティが Loosely に同じかどうかを見ているだけなので、 == とかと同じく、 厳密な型チェックの同一性を確認しない働きをします。

この他にも assert.fail がよくわからない動きをします。

assert.fail("should not be reached") // Throw AssertionError should not be reached

assert.fail は fail するという関数です。本来的な使い方で言うと、 test でここに来ちゃったら fail ですよっていうときにメッセージを出す目的で使います。

ただ2つの引数を渡すと第1引数を actual 、第2引数をexpected として Error にメッセージを詰めて throw してくれます(そんな機能いらない)。

assert.fail("a", "b") // Throw AssertionError a != b
assert.fail("a", "a") // Throw AssertionError a != a

ちなみに assert.fail に引数を2つ以上渡すことは 今でも deprecated になっています。

https://nodejs.org/dist/latest-v17.x/docs/api/assert.html#assertfailactual-expected-message-operator-stackstartfn

そんなこんなで、いくつかの関数が意図しない動きをするので昔は deprecated でした。assert モジュールを使うことは 今は deprecated ではありません。

strict method が出てくる。

この状況を改善するべく、 strictEqual と deepStrictEqual というメソッドが出てきます。これは前述した型が一致しているかまで見る assert method になっています。普通にこっちを使うほうが良いです。

assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]); // OK 3 と '3' は == では一致するため

assert.deepStrictEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]); // Throw Assert Error

ただ細かい注意点としては、 deepStrictEqual は厳密等価演算子である === と同じではありません。 どちらかというと、 同値かどうかを表しています。 ECMAScript 的に言うと Same Value かどうかなので、 Object.is() の deep 比較と似ています。 そのため、 NaN 同士の比較も true になります。

assert.deepStrictEqual(NaN, NaN); // OK NaN と NaN は === で違うが、 Object.is() では true のため。
assert.deepStrictEqual(+0, -0); // Throw Assert Error +0 と -0 は Object.is() では false のため。

(それじゃあ deepSameValue のが良いのではないか?と思って、一応 issue で提案したんですが、まぁ微妙な議論になりそうだったので、引き下がりました。)

strict assertion mode が出てくる。

こんな感じで deepStrictEqual が出てきてメデタシメデタシではあったのですが、 deepStrictEqual というメソッドが微妙に長くて使いにくいです。 あと、 strictDeepEqual だっけ? deepStrictEqual だっけ?ってどっちがどっちかわからなくなったりします。そもそも deepEqual 微妙だよねっていう話もあり、最近では "assert/strict" の方を使って、 deepEqual でも deepStrictEqual と同じ動きになるように変更されています。

import assert from "node:assert/strict";

assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]);

+ actual - expected ... Lines skipped

  [
    [
      [
        1,
        2,
+       3
-       '3'
      ]
...
    4,
    5
  ]
    at file:///private/tmp/test/strict.mjs:3:8
    at ModuleJob.run (node:internal/modules/esm/module_job:195:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:337:24)
    at async loadESM (node:internal/process/esm_loader:88:5)
    at async handleMainPromise (node:internal/modules/run_main:65:12) {
  generatedMessage: true,
  code: 'ERR_ASSERTION',
  actual: [ [ [ 1, 2, 3 ] ], 4, 5 ],
  expected: [ [ [ 1, 2, '3' ] ], 4, 5 ],
  operator: 'deepStrictEqual'
}

まぁ正直どっち使っても良いんですが、 assert/strict にして deepEqual つかうか、 assert にして deepStrictEqual 使うかという選択肢が増えています。

まとめ

assert に strict assertion mode ができたよっていう話でした。