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

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

* [Cloud Firestore セキュリティ ルールをテストする  |  Firebase](https://firebase.google.com/docs/firestore/security/test-rules-emulator?hl=ja)
* [ローカルでの関数の実行  |  Firebase](https://firebase.google.com/docs/functions/local-emulator?hl=ja)

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

* [Build unit tests  |  Firebase](https://firebase.google.com/docs/rules/unit-tests)

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

* [javascript - How to use firestore emulator from client - Stack Overflow](https://stackoverflow.com/questions/57537017/how-to-use-firestore-emulator-from-client)

## なんか動かない

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

```typescript: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!' });
  });
})
```

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

```console
  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` がタイムアウトして落ちているようです。サンプルでいうと、↓のデータベースをクリアしている部分ですね。

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

`clearFirestoreData` が止まってしまったのだと思い、しばらくソースコードを読んでいたりしたのですが、[エミュレータ向けのgRPCクライアントの初期化](https://github.com/firebase/firebase-js-sdk/blob/493fe52d74b43200bc4e5afcc8f1f13495d66a0b/packages/testing/src/api/index.ts#L235)の行で終わるということまでしかわからず、途方にくれていました。

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

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

```console
Error: Timeout of 2000ms exceeded.
```

私もそこに引っ張られて「そうだよな……2秒もかけてダメってことは、たぶんどこかで処理が止まったんだよな……」という前提で調査を進めていましたが、ふと[このIssue](https://github.com/firebase/firebase-js-sdk/issues/2090)が `mocha --timeout=10000` を設定しているのを見て、「あれ、本当に2秒以上かかっているのでは？」と思い直しました。

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

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

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

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

  1 passing (20s)
```

やったー通ったー🎉

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

## まとめ

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

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