The Firebase Blog: Launching Cloud Functions for Firebase v1.0 で、 Easier unit testing
という項目がありました。
firebase/firebase-functions-test という npm が公開されたので、これは実際のところ何ができるのか調べてみました。(Firestore 目線での調査です。)
手探りでやっているため、間違いがあったらマサカリ歓迎です。
firebase-functions-test/firestore.ts
firebase-functions-test/firestore.ts の interface はこれだけです。
単に DocumentSnapshot
を作るだけの Utility ですね。
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 という、 RealtimeDB の offline テストのサンプルがあります。
しかし Firestore のサンプルはないので、 RealtimeDB のサンプルを Firebase に書き換えてみます。
ここでは、 jest + TypeScript でテストを書いています。
テスト対象の Function
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.` })
})
})
テストコード
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 のように実際に online でデータを書き込む方法のが実用的
- (functions-samples/test.offline.js では
sinon.stub(admin, 'initializeApp')
しているが、今回はしなくても動いた)
おわり
offline でテストを書くなら、現時点では soumak77/firebase-mock が良い気がします。これは offline で動作する Firestore Database を提供してくれます、一部バグがありますが。(Firestore だけでなく、 RealtimeDB などもローカルで動かせます。)
soumak77/firebase-mock についてはまた別で記事を書こうと思います。
makeUppercase のテストも gist に置いておきます。
最初にも書きましたが、他に参考になるテストコードサンプルなどがなく気合いで書いたので、間違っていたらコメントいただけると助かります。