Cloud Functionsを書く際に、簡単な関数ならテストなしでも書けますが、少し複雑になってくるとテストがあると作るのが楽です。
この記事では、JestとTypeScriptを使ってCloud Functionsのテストを書く準備を行う方法をご紹介します。
公式ドキュメントでは mocha を使っていますが、今回は普段使っているJestを採用しました。
前提として firebase init で functions ありで TypeScript で初期化されているものとします。
まずは必要なnpmパッケージを追加します。
npm install --save-dev firebase-functions-test jest @types/jest ts-jest sinon @types/sinon
jestをTypeScriptで使うためにts-jestを入れています。
% npx ts-jest config:init
こちらを実行することで jest.config.js が自動生成されます。
特に内容を変更する必要はありませんでした。
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
テストはsrcフォルダーに入れる方式にしました。
例えば notifyArticle.ts があったとすると
notifyArticle.test.ts というファイル名でテストを作成します。
上記の手法でやると% firebase deploy
の際に
テストがデプロイ対象ファイルになってしまって
面倒なのでtestフォルダーに分けたほうが良かったです。
test/notifyArticle.test.ts となるようにしました。
package.json の scripts に "test": "jest"
を追加します。
例としてFCMを利用して通知を送るプログラムのテストを作成してみました。
import { FeaturesList } from 'firebase-functions-test/lib/features'
import * as sinon from 'sinon'
import * as admin from 'firebase-admin'
let test: FeaturesList
let myFunctions: any
let adminMessageSend: sinon.SinonStub<[admin.messaging.Message, (boolean | undefined)?]>
beforeEach(() => {
test = firebaseFunctionsTest({
databaseURL: "https://[PROJECT_ID].firebaseio.com",
projectId: "[PROJECT_ID]"
}, "[サービスアカウントの秘密鍵のPATH]")
myFunctions = require('../src/index')
});
afterEach(() => {
test.cleanup()
});
describe('notifyArticle', () => {
beforeEach(async () => {
// テストでは実際にFCMは送れないため、sinonを使ってstubを作成しています
adminMessageSend = sinon.stub(admin.messaging(), "send")
})
afterEach(async () => {
// sinon.stubは重複して行えないため毎回restoreします
await adminMessageSend.restore()
})
it('nominal scenarios', async () => {
// test.wrapでテスト対象にしたい関数を包みます
const wrapped = test.wrap(myFunctions.notifyArticle)
// このように前提になるドキュメントを作成する事ができます
await admin.firestore().collection("users").doc("0").set({})
// onCreateに送りつけるドキュメントを生成する事ができます
const snap = test.firestore.makeDocumentSnapshot({}, 'articles/0123456789');
// Functionsは複数回呼ばれる可能性があるので2回以上実行します
await wrapped(snap, { eventId: "EVENT_ID"})
await wrapped(snap, { eventId: "EVENT_ID"})
// この関数ではFCMを飛ばそうとしているので
// sinonを使用してstubが何回呼ばれた・どんな引数で呼ばれたかについて検証するようにしています
expect(adminMessageSend.callCount).toEqual(1)
expect(adminMessageSend.getCall(0).args[0]).toEqual({
notification: {
title: 'ARTICLE_TITLE',
body: 'ARTICLE_BODY'
},
token: 'FCM_TOKEN'
})
// テストで使用したデータを削除します
await admin.firestore().collection('users').doc('0').delete()
await admin.firestore().collection('systemEvents').doc('EVENT_ID').delete()
})
})