9
8

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.

Cloud Functions for Firebase入門 (簡単なテストまで)

Last updated at Posted at 2019-12-30

モチベーション

最近Cloud Functions for Firebaseの開発を少しやってみて色々右往左往してたので、初心者なりにフローをまとめてみました。
フロント側を最近Vueを使った開発を行っており、極力現在のフロント環境(es6, webpackによるバンドル化など)と合わせる構成にしています。

ゴール

firebase functinonsの開発/運用するために以下の点をできるようにする

  • デバッグ(ローカル上での実行およびホットリロード)
  • テスト
  • デプロイ

Cloud Functions for Firebaseとは

GCPサービスとして提供されているCloud Functionsを簡単にfirebaseの各種サービスと連携して使える、いわゆるFaasサービスです。詳しく書くとにわかがばれそうですが、公式リファレンスをみることをおすすめします。

作成したもの

こちらにサンプルをおいています。これを通して説明をしていきます。
また、firebaseの設定に関しては深くはふれません。今回は.firebasercに予め開発用プロジェクトIDを設定し、firebase loginも済んでいる状態とします。

また以下のツール、フレームワークを利用しました。

  • express (ルーティングロジック簡略化のため)
  • webpack (フロント環境と記法をあわせるため)
  • jest (テストの利用)

ディレクトリ構成

今回は下記のような構成にしました。

  • プロジェクトルートディレクトリにpackge.jsonなどの設定ファイル
  • src/*配下に開発用のソースコード
  • test/*配下にテストコード
︙
├── firebase.json
├── node_modules
├── package.json
├── src
│   └── main.js
├── tests
│   └── main.test.js
├── babel.config.js
└── webpack.config.js

しかし、デフォルトはfunctionsディレクトリ以下にfirebase.jsonpackage.jsonとエントリーポイントなjs(例はindex.js)が存在することが求められており、今回はwebpackで無理やり解決しました。

├── functions
│   ├── firebase.json
│   ├── package.json
│   ├── node_modules
│   └── index.js
├── package.json
├── babel.config.js
└── webpack.config.js

ビルド設定

webpack.config.js


const nodeExternals = require('webpack-node-externals');
const GenerateJsonPlugin = require('generate-json-webpack-plugin')

const env = process.env.NODE_ENV;
const dist = "functions";
const distDir =`${__dirname}/${dist}`;
const mode = env && env == "prod" ? "production" : "development";

module.exports = {
    mode: mode,
    target: 'node',
    entry: ["./src/main.js"],
    output: {
        filename: "index.js",
        path: distDir
    },
    plugins: [
        //firebaseは各種jsonをfunctions内に入れないといけないのでコピー
        new GenerateJsonPlugin('package.json', require('./package')),
        new GenerateJsonPlugin('firebase.json', require('./firebase'))
    ],
    watchOptions: {
        poll: 1000
    },
    externals: [nodeExternals({
        whitelist: []
    })]
};

基本的にはHow to build firebase function by webpackを参考にしました。多分以下の点が肝かなと思います。

アプリケーションコード

アプリケーションコード
hello worldを返すだけのシンプルなものです。
expressを使った書き方に関してはCloud Functions for Firebaseでexpressを使うの記事を参考にさせていただきました。


import express from "express"
import cors from "cors";
import * as functions from "firebase-functions"
import * as bodyParser from "body-parser";

const app = express();
app.use(cors({origin: true}));

app.get('/', bodyParser.urlencoded({extended: false}), (req, res) => {
    return res.send("hello world");
});

exports.sample = functions.https.onRequest(app);

クロスオリジン設定は今後の開発で私が必要なので設定を追加しているだけなので、不要なら削除してください。


app.use(cors({origin: true}));

なお、今回はfunctions名をsampleとしてします。export.{functions名}で分けられるようですね。


exports.sample = functions.https.onRequest(app);

テストコード

テストコード


import supertest from 'supertest'
import functions from "../src/main"

const request = supertest(functions.sample);

jest.mock('firebase-admin', () => ({
    initializeApp: jest.fn()
}));

describe('hello world sample', () => {
    it('successfully invokes function', async () => {
        let actual = await request.get('/');
        let {ok, status, body, text} = actual;
        expect(ok).toBe(true);
        expect(status).toBeGreaterThanOrEqual(200);
        expect(body).toBeDefined();
        expect(text).toEqual("hello world");
    });
});

今回はユニットテストの体なのでいわゆるオフラインテストになります。firebaseにアクセスはしませんので、認証系は握り潰す必要があるので握り潰しました。


jest.mock('firebase-admin', () => ({
    initializeApp: jest.fn()
}));

また、本来は公式テストフレームワークのfirebase-functions-testを使いたかったのですが、expressを使ったエンドポイントがうまく機能してくれなかったのでこちらを参考にHttpテストライブラリのsupertestを使いました。


describe('hello world sample', () => {
    it('successfully invokes function', async () => {
        let actual = await request.get('/');
        let {ok, status, body, text} = actual;
        expect(ok).toBe(true);
        expect(status).toBeGreaterThanOrEqual(200);
        expect(body).toBeDefined();
        expect(text).toEqual("hello world");
    });
});

簡単に書けるのいいですね。

es6でテストが書けるようにjest.config.jsに設定を追加させています。
こちらをほぼコピペしています。


module.exports = {
    moduleFileExtensions: ["js", "json", "jsx", "ts", "tsx", "json"],
    transform: {
        '^.+\\.(js|jsx)?$': 'babel-jest'
    },
    testEnvironment: 'node',
    moduleNameMapper: {
        '^@/(.*)$': '<rootDir>/$1'
    },
    testMatch: [
        '<rootDir>/**/*.test.(js|jsx|ts|tsx)', '<rootDir>/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))'
    ],
    transformIgnorePatterns: ['<rootDir>/node_modules/']
};

async/awaitと使っているとReferenceError: regeneratorRuntime is not definedのエラーが発生するので、
babel.config.jsに下記のように現在のnodeのバージョンの指定もしておく事も忘れずに必要です。(参考: async/awaitを使用して、"regeneratorRuntime is not defined "エラーが出た時の対処)


module.exports = {
    "presets": [
        [
            "@babel/preset-env", {
            "targets": {
                "node": "current"
            }
        }
        ]
    ]
}

実行と結果

実行コマンドはpackage.jsonのscriptに集約させるようにしました。
ゴール設定していた、デバッグ, テスト, デプロイに関して解説します。
前述しましたが、今回は.firebasercに予め開発用プロジェクトIDを設定し、firebase loginも済んでいる状態とします。


"scripts": {
    "start": "npm run watch & npm run serve",
    "watch": "NODE_ENV=dev webpack -w",
    "build": "NODE_ENV=dev webpack",
    "release": "NODE_ENV=prod webpack",
    "serve": "npm run build && firebase serve  -o 0.0.0.0 -p 8080 --only functions",
    "deploy": "npm run release && firebase deploy --only functions:sample",
    "lint": "eslint src/* tests/*",
    "test": "jest"
  },

デバッグ

$ npm run start

serve自体をwatchせずに動かす場合もあると思ってるので、watchserveは一応別コマンドで作成しています。

デバッグ結果 塗り部した箇所は開発用プロジェクトなので各自読み替えてください。 `http function initialized`以下の`(http://0.0.0.0:8080///sample)`にアクセスするとローカルで実行中のfunctionsにアクセスできるはずです。 `src/main.js`のファイルを編集するとそれに応じて内容もかわるはずです。

テスト

$ npm run test
テスト結果 無事にテストが通っていますね。 環境変数が設定されておらずで怒られますが、ローカルの実行のみなので今回は割愛させていただきます。

deploy

$ npm run deploy
スクリーンショット 2019-12-30 23.29.49.png

なお今回デプロイするにあたって、firebase deploy --only functions:sample"とデプロイする関数名を明記しています。理由はデプロイする対象のものは明示的に指定するほうが誤った削除を防ぐ意味で事故防止になって良いかなと考えています。

感想

一通り運用のための最低限のことは最低限できました。模索しながら色々やったのでほぼ1日費やしてしまいましたが、サクッと試す分は5分もかからずできるのでfirebase大好きになりました。

webpack周りで結構ハマったので、es6にこだわず素直にcommonjsで素直に書けば良かったかもですね。そこまでやるならおとなしくtype scriptをやるべきだったなと反省しました。

なお、ほとんどJSでテストコード書いたことがなく、ほぼ今回始めてだったのですがjestは学習コスト低そうで活用していきたいですね。jsのテストフレームワークを調べると色々組み合わせがあるようで辟易してたのですが、jestが色々できそうなのでまずは寄せて書くことから始めてみようとおもいました。

余談ですが、testには本来はfirebase-testを使いたかったのですが、下記のところで怒られており、誰か教えてください。作成した関数に__trigger.labels.deployment-callableはなかったです。
https://github.com/firebase/firebase-functions-test/blob/master/src/main.ts#L75


if (has(cloudFunction, '__trigger.httpsTrigger') &&
      (get(cloudFunction, '__trigger.labels.deployment-callable') !== 'true')) {
    throw new Error('Wrap function is only available for `onCall` HTTP functions, not `onRequest`.');
  }
9
8
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
9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?