今日からできるCloud Functions for Firebaseでconsole.logデバッグ風味のオンラインテスト

[Roppongi.js #2](https://roppongi-js.connpass.com/event/82998/)

2018年4月24日

---

## 自己紹介

ちきさん

Twitter/GitHub/Qiita: @ovrmrw 

市ヶ谷のオプトという会社で働いています。初心者です。

![3a2512bb-aa72-4515-af42-1f1721252f39.jpg](https://qiita-image-store.s3.amazonaws.com/0/74793/81628787-0297-a046-e6ed-96cc1094bff0.jpeg)

---

今回のGitHubリポジトリ

https://github.com/ovrmrw/firebase-cloud-functions-local-testing

---

## 「Cloud Functions for Firebase」とは

> Cloud Functions for Firebase を使用すると、Firebase 機能や HTTPS リクエストによってトリガーされたイベントに応じて、バックエンド コードを自動的に実行できます。コードは Google のクラウドに保存され、マネージド環境で実行されます。独自のサーバーを管理およびスケーリングする必要はありません。

( https://firebase.google.com/docs/functions/?hl=ja より)

---

## 何がうれしいのか

詳しくは [Cloud Functions で可能な処理 (公式)](https://firebase.google.com/docs/functions/use-cases?hl=ja) で。

---

### 要するにAWS Lambdaみたいなやつです。 名前が長いのでここから先は **Cloud Function** と呼びます。

---

## Cloud Functionを書いているときのつらみ

- **実際にクラウド上で動かしてみないと**データが思い通りに流れているか**わかりにくい。**
- とはいえ**「コード直して」「デプロイして」「手動でイベント発生させて」「ログ確認して」の繰り返し**は精神的にも時間的にも**つらい。**:innocent:

---

## そこで オンラインテスト

- **「どうなるはず」**を書くより、**「実際にどうなったか」**を書く方が手っ取り早い。(個人の意見)
- テストを書くというより、実際に起きた結果を確認しながらアサーションを書いていたらテストを書いていた、みたいな感じ。
- (推奨されている手法かどうかはわからない)

---

## オンラインテスト

---

## オンラインテストの基本形

```js
const Test = require('firebase-functions-test');

describe('.....', () => {
  let test;

  beforeEach(() => {
    test = Test(JSON.parse(process.env.FIREBASE_CONFIG));
  });

  it('.....', async () => {
    // Realtime Databaseの場合
    const snapshot = test.database.makeDataSnapshot(value, refPath);
    await test.wrap(your_cloud_function)(snapshot);
  });
});
```

(ref: https://github.com/firebase/firebase-functions-test )

---

## itの中身をクローズアップ

```js
const snapshot = test.database.makeDataSnapshot(value, refPath);
await test.wrap(your_cloud_function)(snapshot); // Cloud Functionを発火させる。
```

- `makeDataSnapshot()`に値を渡して`DataSnapshot`型のオブジェクトを作る。
- `wrap()`にCloud Functionを渡してイベント発生をシミュレートするWrapped Functionを作り、その引数に`DataSnapshot`を渡す。
- これによりCloud Functionがイベント駆動する様子を**ローカルで再現できる。**

---

## オンラインテストの例 (Database => PubSub)

- Databaseの`onCreate`イベントをトリガーにする。
- Cloud Function発火後、Subscriberが`pull`してTopicにメッセージが送られたことをアサーションする。

```js:pubsub.spec.js
import * as Test from 'firebase-functions-test';
import { PubsubHelper } from '../../testing/helpers';
import { pubsub, topicName } from '../../src/pubsub';

jest.setTimeout(1000 * 30);

describe('pubsub', () => {
  let test;
  let pubsubHelper;
  const subscriptionName = 'sample-subscription__test';

  beforeEach(() => {
    test = Test(JSON.parse(process.env.FIREBASE_CONFIG));
    pubsubHelper = new PubsubHelper();
  });

  beforeEach(async () => {
    // initialize testing environment
    await pubsubHelper.deleteSubscription(subscriptionName);
    await pubsubHelper.createSubscription(subscriptionName, topicName);
  });

  it('Topicにメッセージが送られる。', async () => {
    const value = { bar: 1 };

    // trigger
    const snapshot = test.database.makeDataSnapshot(value, '');
    await test.wrap(pubsub)(snapshot);

    // pubsub message assertion
    const messages = await pubsubHelper.pullMessages(subscriptionName);
    expect(messages.length).toBe(1);
    const firstMessage = JSON.parse(messages[0].message.data.toString());
    expect(firstMessage).toEqual(value);
  });
});
```

(ref: https://github.com/ovrmrw/firebase-cloud-functions-local-testing/blob/master/functions/__tests__/online/pubsub.spec.ts )

---

## オンラインテストの Tips

---

## テスト用のプロジェクトを別に作る

- テスト用のプロジェクトにはCloud Functionを**デプロイしてはいけない。**
- なぜならオンラインテストのとき、イベントに対してローカルのFunctionとデプロイしたFunctionの**両方が動いてしまい**、まともな結果が得られなくなるから。(これで超嵌った)
- なのでデプロイせずにテストするために**プロジェクトを別に用意する必要がある。**

(ref: [Firebaseプロジェクトを本番用とテスト用で使い分けたい](https://qiita.com/ovrmrw/items/1a45042d06db5d343471))

---

## Cloud Functionのトリガーとなる副作用は(必要なら)自前で発生させる

```js
const snapshot = test.database.makeDataSnapshot(value, refPath);
await snapshot.ref.set(snapshot.val()); // <==(必要なら)事前にDatabaseに書き込む。
await test.wrap(your_cloud_function)(snapshot); // Cloud Functionを発火させる。
```

- firebase-functions-test を使うとCloud Functionをトリガーすることはできるが、あくまでも**イベントが発生したことをシミュレートするだけ**で実際にイベントは起きていない。(ちゃんと理解してないだけかも...)
- 例えばDatabaseの`onCreate`イベントを発生させることはできるが、実際にDatabaseには書き込まれていない。
- なのでCloud Functionの中でデータを利用・更新する場合は、**事前に書き込みをしておく必要がある。**

---

## Jestのタイムアウトを長めにする

```js
jest.setTimeout(1000 * 30);

describe('.....', () => {
  // テスト
});
```

- Jestのデフォルトタイムアウトは5秒。
- オンラインテストはクラウド上で副作用を発生させるので**5秒では終わらないことが多い。**(特にPubSub)
- `it`だけタイムアウト時間を変更しても`beforeEach`でタイムアウトしたりするので全体に適用できる`jest.setTimeout()`を使うと便利。

---

## ちょっと待たないとTerminalにログが残らない (ときがあった)

```js
it('.....', async () => {
  const snapshot = test.database.makeDataSnapshot(value, refPath);
  await test.wrap(your_cloud_function)(snapshot); 
  await new Promise(resolve => setTimeout(resolve, 1000)); // ちょっと待つ
});
```

- Cloud Functionを実行した後、ログを全てTerminalに表示させるためにちょっと待つ。
    - (再現できなかったけど待たないとログが残らないときがあった。。。)

---

## まとめ

---

## メリット

- **「どうなるはず」**ではなく**「実際にどうなったか」**をアサーションしているので、本番に適用しても**問題なく動くことが期待できる。**
- 実際に流れている値をconsole.logで確認できる。

## デメリット

- 全てをモックするオフラインテストに比べれば単純に時間がかかる。
- クラウド上で動作させるので、たまに通るはずのテストが落ちることがある。

---

## うれしいこと

- 作業効率は上がる、と思う、たぶん。
- 少なくとも**ログを確認するためにデプロイして手動でイベント発生させるのを繰り返す**という不毛な戦いは避けられる。
- console.logデバッグをする感覚でコードを書ける。

---

## 今朝知ったこと ←NEW!

- 公式のドキュメントが4月20日に更新されていてそっちにだいたい書いてあった。
    - https://firebase.google.com/docs/functions/unit-testing

---

# Thanks :raised_hands: