学ぶ、考える、書き出す。

学習し、自分なりに噛み砕いて、書き出すブログ。

AVA上でsinon.useFakeTimers()を複数のテスト内で実行するとエラーが出る

最近Sinon.JSのバージョンをv14.0.0に上げたときに、エラーが出てテスト実行が失敗するようになりました。

この記事では対処方法を書いていきます。

事象

たとえば次のようなテストがあったとします。

sinon.useFakeTimersドキュメントにある通り、setTimeout や clearTimeoutなどを置き換える関数です。

import test from 'ava';
import sinon from 'sinon';

test('test', (t) => {
    const fakeTimer = sinon.useFakeTimers();
    t.pass();
    fakeTimer.restore();
});

test('test2', (t) => {
    const fakeTimer = sinon.useFakeTimers();
    t.pass();
    fakeTimer.restore();
});

2回目の sinon.useFakeTimers() を実行するとき、先ほどのテストファイルで言うと test2 を実行するときに次のようなメッセージが表示されます。

TypeError {
  message: 'Can\'t install fake timers twice on the same global object.',
}

原因

書き換えられたグローバルオブジェクトを戻すために sinon.useFakeTimersfakeTimer.restore() を対になる形で実行しないといけません。

うっかり fakeTimer.restore() を実行しないまま sinon.useFakeTimers を実行すると、元の日時に復元することが難しくなります。

// サンプル
const sinon = require("sinon@12.0.0")

console.log("Original time: " + new Date().getTime()); // "Original time: 1653007080412"
let fakeTimer = sinon.useFakeTimers(Date.parse("2014-06-05T12:07:07.662Z"));
fakeTimer = sinon.useFakeTimers(Date.parse("2018-04-11T14:08:00Z"));
fakeTimer.restore();
console.log("Restored time: " + new Date().getTime()); // "Restored time: 1401970027662"

今回のサンプルコードの場合はまだ復元できると思いますが、これがより回数を重ねて sinon.useFakeTimers を実行してしまうとより復元が難しくなります。

この問題が、Impossible to restore fake timers in certain situations. · Issue #2449 · sinonjs/sinonで報告されて、対応として @sinonjs/fake-timers 側でProhibit faking of faked timers by cjbarth · Pull Request #426 · sinonjs/fake-timersというPull Requestがマージされました。

Pull RequestのSolutionに「If an attempt is make to fake a timer that is already faked, an exception will be thrown.」と書いてある通り、timerがすでにfakeだった場合に再度 sinon.useFakeTimers を実行した場合に例外が投げられるという変更がされました。

この変更により、複数のテストで sinon.useFakeTimersfakeTimer.restore を実行していた場合に、AVA上でテストが並列で実行されることもあって fakeTimer.restore が実行される前に sinon.useFakeTimers が実行される場合が出てきました。

その結果として「Can't install fake timers twice on the same global object.」というエラーが出力されるようになりました。

解決策

解決方法は2つあります。

まず1つは、テストコード側で並列実行をやめて直列実行にすることです。具体的には次の通り書くとテストが成功します。

import test from 'ava';
import sinon from 'sinon';

test.serial('test', (t) => {
    const fakeTimer = sinon.useFakeTimers();
    t.pass();
    fakeTimer.restore();
});

test.serial('test2', (t) => {
    const fakeTimer = sinon.useFakeTimers();
    t.pass();
    fakeTimer.restore();
});

または test.beforetest.after といったテストファイル内の最初と最後のテスト前後で実行されるフックを使って、fakeTimerを使うのも良いです。

import test from 'ava';
import sinon from 'sinon';

let fakeTimer = null;

test.before(() => {
    fakeTimer = sinon.useFakeTimers();
});

test.after(() => {
	if (!fakeTimer) {
		return;
	}
    fakeTimer.restore();
});

test('test', (t) => {
    t.pass();
});

test('test2', (t) => {
    t.pass();
});

AVAのように並列実行がデフォルトのテストフレームワークだと同じ問題が起きそうですが、他のJestやVitestなどはどうしているのか気になります。

記事を共有する

関連記事

  1. ブログ記事を投稿するためのフォームを作った

    はてなブログのMarkdownによる記事編集画面やesa - 自律的なチームのための情報共有サービスのように、本文の編集画面とリアルタイムプレビューが横並びになるような投稿フォームが個人的に好みです。

  2. ブログを国際化対応した

    当ブログのUIで使う文言を国際化対応しました。

  3. 自分がアクセシビリティ向上に力を入れる理由

    いま自分は、担当サービスのアクセシビリティ向上を推進する「アクセシビリティタスクフォース」を率いる立場になっています(アクセシビリティタスクフォースについては AbemaTV ABEMA iOS版アプリのアクセシビリティ向上支援 | 事例紹介 | 株式会社コンセント で少し触れられています)。

  4. Twitterの右サイドバーを非表示にする

    Twitter のトレンドを見ると、イラつきを覚えるようになりました。そんなにイラつきを覚えるようなら見なければいいし、そもそも Twitter やめろという話はあります。