mocha + @firebase/testingでハマった時にはタイムアウト時間を見直す


Firebaseで開発をしていると、FirestoreのルールやCloud Functionsとの連携など、細々とした設定を安全にテストしたくなりますよね。
そういった用途のために、FirebaseではFirestoreやCloud Functionsのエミュレータが用意されています。

また、上記のエミュレータをテスティングフレームワークから叩くためのヘルパーとして、 @firebase/testing というNode.js向けライブラリも提供されています。

実用の仕方はこちらのSOの回答がわかりやすかったです。

なんか動かない

さて、これは良いツールを知ったと思い、mochaの中で動かすことにしました。前述の記事の内容を組み合わせて、次のようなテストコードを書いてみました。

hoge.test.ts
import assert = require("assert");
import * as firebase from "@firebase/testing";
import "mocha";

const FIRESTORE_PROJECT_ID = "my-project";

function authedApp(auth?: object) {
  return firebase.initializeTestApp({ projectId: FIRESTORE_PROJECT_ID, auth }).firestore();
}

describe("hoge", () => {
  beforeEach(async () => {
    await firebase.clearFirestoreData({ projectId: FIRESTORE_PROJECT_ID });
  });

  afterEach(async () => {
    await Promise.all(firebase.apps().map(app => app.delete()));
  });

  const COLLECTION_NAME = "myCollection";
  it("fuga", async () => {
    const db = authedApp();

    // Manually add item to collection
    const ref = await db.collection(COLLECTION_NAME).add({hello: 'World!'});

    // Fetch item by id 
    const resp = await db.collection(COLLECTION_NAME).doc(ref.id).get();

    assert(resp.exists);
    assert.deepEqual(resp.data(), { hello: 'World!' });
  });
})

これを実行すると、次のような結果になりました。

  hoge
    1) "before each" hook for "fuga"

  0 passing (10s)
  1 failing

  1) hoge
       "before each" hook for "fuga":
     Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/path/to/hoge.test.ts)

fuga テストが始まる前の beforeEach がタイムアウトして落ちているようです。サンプルでいうと、↓のデータベースをクリアしている部分ですね。

  beforeEach(async () => {
    await firebase.clearFirestoreData({ projectId: FIRESTORE_PROJECT_ID });
  });

clearFirestoreData が止まってしまったのだと思い、しばらくソースコードを読んでいたりしたのですが、エミュレータ向けのgRPCクライアントの初期化の行で終わるということまでしかわからず、途方にくれていました。

タイムアウトを延ばせばよかった

mocha先生は「2秒も待ったのに結果が出ないじゃないの! きぃぃぃ!!!!」とブチ切れておられました。

Error: Timeout of 2000ms exceeded.

私もそこに引っ張られて「そうだよな……2秒もかけてダメってことは、たぶんどこかで処理が止まったんだよな……」という前提で調査を進めていましたが、ふとこのIssuemocha --timeout=10000 を設定しているのを見て、「あれ、本当に2秒以上かかっているのでは?」と思い直しました。

10秒で済むとは思えなかったので、雑に --timeout=30000 で実行してみます。また、 beforeEach がどのくらいかかっているのかを調べるべく、次のように計測コードを仕込んでみました。

  beforeEach(async () => {
    const start = Date.now();
    await firebase.clearFirestoreData({ projectId: FIRESTORE_PROJECT_ID });
    console.log(`before each time: ${Date.now() - start}`);
  });

その結果がこちらになります。

  hoge
before each time: 10089
    ✓ fuga (10227ms)

  1 passing (20s)

やったー通ったー🎉

結局、初期化もテスト自体も10秒以上かかっていました。テスト内容にもよりますが、少なくとも15000ms程度のタイムアウトは設定しておいたほうがよさそうです。

まとめ

冷静に考えてみると、Firebaseのローカルエミュレータへのアクセスを伴うということはE2Eテストなので、時間がかかるのは当然といえば当然でした。

@firebase/testing さん、実装を疑ってごめんな……