firebase-functions-test を使って Firestore の offline テストを書いてみる

[The Firebase Blog: Launching Cloud Functions for Firebase v1\.0](https://firebase.googleblog.com/2018/04/launching-cloud-functions-for-firebase-1-0.html) で、 `Easier unit testing` という項目がありました。
[firebase/firebase\-functions\-test](https://github.com/firebase/firebase-functions-test) という npm が公開されたので、これは実際のところ何ができるのか調べてみました。(Firestore 目線での調査です。)

手探りでやっているため、間違いがあったらマサカリ歓迎です。

## [firebase\-functions\-test/firestore\.ts](https://github.com/firebase/firebase-functions-test/blob/master/src/providers/firestore.ts)

[firebase\-functions\-test/firestore\.ts](https://github.com/firebase/firebase-functions-test/blob/master/src/providers/firestore.ts) の interface はこれだけです。
単に `DocumentSnapshot` を作るだけの Utility ですね。

```ts
import { Change } from 'firebase-functions';
import { firestore, app } from 'firebase-admin';
export interface DocumentSnapshotOptions {
    readTime?: string;
    createTime?: string;
    updateTime?: string;
    firebaseApp?: app.App;
}
export declare function makeDocumentSnapshot(
    data: {
        [key: string]: any;
    }, 
    refPath: string, options?: DocumentSnapshotOptions
): any;
export declare function exampleDocumentSnapshot(): firestore.DocumentSnapshot;
export declare function exampleDocumentSnapshotChange(): Change<firestore.DocumentSnapshot>;
```

内部コードを読んでみましたが、他に利用できるコードはなさそうです。
offline テストができると blog には書いてありましたが、 offline で動作するデータベースが提供されたのではなく、テストヘルパーが提供されたという認識が正しいです。

## Realtime Database のテストサンプルを Firestore で書き直す

[functions\-samples/test\.offline\.js](https://github.com/firebase/functions-samples/blob/master/quickstarts/uppercase/functions/test/test.offline.js) という、 RealtimeDB の offline テストのサンプルがあります。
しかし Firestore のサンプルはないので、 RealtimeDB のサンプルを Firebase に書き換えてみます。

ここでは、 [jest](https://facebook.github.io/jest/ja/) + TypeScript でテストを書いています。

#### テスト対象の Function

```ts
export const addMessage = functions.https.onRequest((req, res) => {
  const original = req.query.text
  return admin.firestore().collection('messages').add({ original: original }).then((writeResult) => {
    return res.json({ result: `Message with ID: ${writeResult.id} added.` })
  })
})
```

#### テストコード

```ts
import * as admin from 'firebase-admin'
import * as fft from 'firebase-functions-test'
const ft = fft()
import * as sinon from 'sinon'
import { makeUppercase } from './makeUppercase'

describe('offline', () => {
  describe('addMessage', () => {
    it('should return a 303 redirect', async () => {
      const collection = 'messages'
      const addParam = { original: 'input' }
      const firestoreStub = sinon.stub()
      const refStub = sinon.stub()
      const addStub = sinon.stub()

      sinon.stub(admin, 'firestore').get(() => firestoreStub)
      firestoreStub.returns({ collection: refStub })
      refStub.withArgs(collection).returns({ add: addStub })
      addStub.withArgs(addParam).returns(Promise.resolve({ ref: 'new_ref' }))

      const req = { query: { text: 'input' } } as any
      const res = {
        redirect: (code, url) => {
          expect(code).toBe(303)
          expect(url).toBe('new_ref')
        }
      } as any

      await addMessage(req, res)
    })
  })
})
```

Firestore で書き直してみた感想としては以下で、 私は firebase-functions-test を使って本番の Project の offline テストを書くのは諦めました。

* admin.firestore() を stub して、 collection も stub して... という感じで、結局全てを stub していかなければならない
* `addMessage` は簡単な Function なので良いが、もっと複雑なものは大量の stub が必要
    * Functions 内で複数の Document を触ったり更新する場合は、それを全て stub しなければならない
    * transaction, batch などを stub し続けていくのは現実的ではないし、結局内部コードのほとんどが stub になるのでテストしていると言えるか微妙
* これなら [functions\-samples/test\.online\.js](https://github.com/firebase/functions-samples/blob/master/quickstarts/uppercase/functions/test/test.online.js) のように実際に online でデータを書き込む方法のが実用的
* ([functions\-samples/test\.offline\.js](https://github.com/firebase/functions-samples/blob/master/quickstarts/uppercase/functions/test/test.offline.js) では `sinon.stub(admin, 'initializeApp')` しているが、今回はしなくても動いた)

## おわり

offline でテストを書くなら、現時点では [soumak77/firebase\-mock](https://github.com/soumak77/firebase-mock) が良い気がします。これは offline で動作する Firestore Database を提供してくれます、一部バグがありますが。(Firestore だけでなく、 RealtimeDB などもローカルで動かせます。)
[soumak77/firebase\-mock](https://github.com/soumak77/firebase-mock) についてはまた別で記事を書こうと思います。


makeUppercase のテストも [gist](https://gist.github.com/starhoshi/21d1fb870d485a95c86fe93cfe1ac240) に置いておきます。

最初にも書きましたが、他に参考になるテストコードサンプルなどがなく気合いで書いたので、間違っていたらコメントいただけると助かります。