Node.js のモック機能にタイマーが追加された
◆ 時間経過では関数は実行されなくなり tick 関数で時間を進める
◆ 即時全部実行もできるけど setInterval を使ってると分かりづらい挙動をする
◆ 即時全部実行もできるけど setInterval を使ってると分かりづらい挙動をする
Node.js 20.4 でテスト用のモック機能にタイマーが追加されたようです
最初見たときには Date で現在時刻系のテストし辛いのが改善されるのか と期待したのですが setTimeout 系の話でした
「タイマー」ですからね
有効にするには enable を呼び出さないとだめみたいです
enable せずに tick や runAll を実行すると enable を実行してとエラーになります
引数は省略すると全部のタイマー関数をモックしてくれるようです
タイマー系でこの関数だけをモックしたいってことはあんまりないですし基本は引数なしでいいかもです
指定方法は関数ではなく文字列でということで全部のタイプの setTimeout などが対象です
グローバル以外に node:timers や node:timers/promises でインポートしたものも対象になります
モックを有効にすると実時間経過では関数が呼び出されなくなります
実行順はタイマーを動かしたときと一緒です
setInterval の場合は一番最初の 1 つだけが実行されて残りは実行されないようです
もう一度実行すると続きではなく最初からになってるようで 同じ関数が再度実行されることになります
タイムアウトの時間が同じだと順番に実行されるようです
setTimeout と setInterval が混ざると複雑になります
setInterval の実行があると setTimeout が残っていても runAll がそこで止まります
なので最初は 1, 2 だけです
もう一度 runAll をすると 1 は終了してるので 3 が入ります
setInterval は 2 と 4 が同じタイムアウト時間で前回が 2 だったので 4 になります
さらにもう一度 runAll を実行すると 同じように次は setTimeout の 5 が来ます
setInterval では 6 は他より間隔が長いので来ることはなく 2 か 4 となり 前回が 4 だったので今回は 2 です
これ以降は setTimeout は残っていないので 2 と 4 が交互に続きます
個人的にはこうであってほしいです
1000 に setInterval が含まれるので 1000 で一旦終了となり 1000 で実行される 1, 2, 3, 4, 5 が実行されます
次の実行ではまた 1000 進んで 2 と 4 さらに合計が 2000 なので 6 も含まれます
その次は 2 と 4 は含まれますが合計では 3000 なので 6 は含まれません
さらにその次は 4000 なので 2, 4, 6 という感じです
最初の時点で 6 まで出ていいとも思うのですが それだと 2 秒経ってることになるので 1 秒で実行するものは 2 回実行されていないと困る場合もありそうです
それに対応すると 1 つ毎日みたいなタイマーが混ざると毎秒の処理が 86400 回行われることになりますし runAll と言っても途中で止めるのは仕方ないのかなと思います
まだ experimental ステータスですし もう少し改善してほしいですね
最初見たときには Date で現在時刻系のテストし辛いのが改善されるのか と期待したのですが setTimeout 系の話でした
「タイマー」ですからね
enable
test 関数で呼び出される関数に引数として渡される TestContext を使って ctx.mock.timers でアクセスできるものです有効にするには enable を呼び出さないとだめみたいです
ctx.mock.timers.enable(["setTimeout"])
enable せずに tick や runAll を実行すると enable を実行してとエラーになります
引数は省略すると全部のタイマー関数をモックしてくれるようです
タイマー系でこの関数だけをモックしたいってことはあんまりないですし基本は引数なしでいいかもです
指定方法は関数ではなく文字列でということで全部のタイプの setTimeout などが対象です
グローバル以外に node:timers や node:timers/promises でインポートしたものも対象になります
モックを有効にすると実時間経過では関数が呼び出されなくなります
test("test", async ctx => {
const _setTimeout = setTimeout
ctx.mock.timers.enable()
let n = 0
setTimeout(() => {
n++
}, 1000)
await new Promise(r => _setTimeout(r, 1200))
assert.strictEqual(n, 0)
})
tick
tick を使って時間を進めますtest("test", ctx => {
ctx.mock.timers.enable()
let n = 0
setTimeout(() => {
n++
}, 1000)
ctx.mock.timers.tick(500)
assert.strictEqual(n, 0)
ctx.mock.timers.tick(500)
assert.strictEqual(n, 1)
})
runAll
runAll を使うと全部のタイマーを即時実行できます実行順はタイマーを動かしたときと一緒です
test("test", async ctx => {
ctx.mock.timers.enable()
const arr = []
setTimeout(() => arr.push(1), 3000)
setTimeout(() => arr.push(2), 1000)
setTimeout(() => arr.push(3), 2000)
setTimeout(() => arr.push(4), 2000)
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [2, 3, 4, 1])
})
setInterval の場合は一番最初の 1 つだけが実行されて残りは実行されないようです
もう一度実行すると続きではなく最初からになってるようで 同じ関数が再度実行されることになります
test("test", async ctx => {
ctx.mock.timers.enable()
const arr = []
setInterval(() => arr.push(1), 1800)
setInterval(() => arr.push(2), 1000)
setInterval(() => arr.push(3), 1500)
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [2])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [2, 2])
})
タイムアウトの時間が同じだと順番に実行されるようです
test("test", async ctx => {
ctx.mock.timers.enable()
const arr = []
setInterval(() => arr.push(1), 1000)
setInterval(() => arr.push(2), 1000)
setInterval(() => arr.push(3), 1000)
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2, 3])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2, 3, 1])
})
setTimeout と setInterval が混ざると複雑になります
test("test", async ctx => {
ctx.mock.timers.enable()
const arr = []
setTimeout(() => arr.push(1), 1000)
setInterval(() => arr.push(2), 1000)
setTimeout(() => arr.push(3), 1000)
setInterval(() => arr.push(4), 1000)
setTimeout(() => arr.push(5), 1000)
setInterval(() => arr.push(6), 2000)
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2, 3, 4])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2, 3, 4, 5, 2])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2, 3, 4, 5, 2, 4])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2, 3, 4, 5, 2, 4, 2])
})
setInterval の実行があると setTimeout が残っていても runAll がそこで止まります
なので最初は 1, 2 だけです
もう一度 runAll をすると 1 は終了してるので 3 が入ります
setInterval は 2 と 4 が同じタイムアウト時間で前回が 2 だったので 4 になります
さらにもう一度 runAll を実行すると 同じように次は setTimeout の 5 が来ます
setInterval では 6 は他より間隔が長いので来ることはなく 2 か 4 となり 前回が 4 だったので今回は 2 です
これ以降は setTimeout は残っていないので 2 と 4 が交互に続きます
期待するのは
これは本当に意図的な挙動なのか疑いたいほどにはおかしな挙動だと思います個人的にはこうであってほしいです
test("test", async ctx => {
ctx.mock.timers.enable()
const arr = []
setTimeout(() => arr.push(1), 1000)
setInterval(() => arr.push(2), 1000)
setTimeout(() => arr.push(3), 1000)
setInterval(() => arr.push(4), 1000)
setTimeout(() => arr.push(5), 1000)
setInterval(() => arr.push(6), 2000)
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2, 3, 4, 5])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2, 3, 4, 5, 2, 4, 6])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2, 3, 4, 5, 2, 4, 6, 2, 4])
ctx.mock.timers.runAll()
assert.deepStrictEqual(arr, [1, 2, 3, 4, 5, 2, 4, 6, 2, 4, 2, 4, 6])
})
1000 に setInterval が含まれるので 1000 で一旦終了となり 1000 で実行される 1, 2, 3, 4, 5 が実行されます
次の実行ではまた 1000 進んで 2 と 4 さらに合計が 2000 なので 6 も含まれます
その次は 2 と 4 は含まれますが合計では 3000 なので 6 は含まれません
さらにその次は 4000 なので 2, 4, 6 という感じです
最初の時点で 6 まで出ていいとも思うのですが それだと 2 秒経ってることになるので 1 秒で実行するものは 2 回実行されていないと困る場合もありそうです
それに対応すると 1 つ毎日みたいなタイマーが混ざると毎秒の処理が 86400 回行われることになりますし runAll と言っても途中で止めるのは仕方ないのかなと思います
まだ experimental ステータスですし もう少し改善してほしいですね