12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Firebase Cloud Functionsの単体テストでJestとTypeScriptを使うセットアップ等

Last updated at Posted at 2020-09-08

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()
  })
})

参考記事

関連記事

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?