Firebase Cloud Functions で Twitter API と連携してみる

[Vue.js で作ったフィードリーダー](https://qiita.com/tanakaoriginal/items/2d6c3942dc07b8dd6e37) で Tweet を取得するための API を [Cloud Functions for Firebase](https://firebase.google.com/docs/functions/) で作ってみました。

実装サンプル： [GitHub](https://github.com/tanakaoriginal/firebase-functions-twitter-sample)

## やりたかったこと

今回は以下の要件とともに Cloud Functions を実装してみました。

* CORS/cache などのミドルウェアを使いたい。
* API エンドポイントとモジュールの単体テストをしたい。
* 自動テストで単体テストを回したい。

## API を動かすまでの手順

リポジトリの [プロジェクトのセットアップ手順](https://github.com/tanakaoriginal/firebase-functions-twitter-sample/blob/master/README.md) を補足しながら追っていきます。

### 必要なもの

* Twitter API を使うためのクレデンシャル
[Twitter Application Management ](https://apps.twitter.com/) から取得します。

* Firebase プロジェクトの [料金プラン](https://firebase.google.com/pricing/) を有料版に変更
外部 API と通信するため、Flame もしくは Blaze に変更します。

* [Firebase Tools](https://firebase.google.com/docs/cli/) のインストール
* [Firebase Console](https://console.firebase.google.com/) での Firebase プロジェクト作成

### Firebase 環境変数の設定

Twitter のクレデンシャルと API で使用する設定を　Firebase　CLI で環境変数に設定します。

```
$ firebase functions:config:set \
credential.twitter.consumer_key="YOUR_CONSUMER_KEY" \
credential.twitter.consumer_secret="YOUR_CONSUMER_SECRET" \
credential.twitter.access_token_key="YOUR_ACCESS_TOKEY_KEY" \
credential.twitter.access_token_secret="YOUR_ACCESS_TOKEY_SECRET" \
... 省略 ...
```

設定の確認

```
$ firebase functions:config:get
{
  "credential": {
    "twitter": {
      "consumer_key": "YOUR_CONSUMER_KEY",
      "consumer_secret": "YOUR_CONSUMER_SECRET",
      "access_token_secret": "YOUR_ACCESS_TOKEY_SECRET",
      "access_token_key": "YOUR_ACCESS_TOKEY_KEY",
      ... 省略 ...
    }
  }
}
```

ローカル環境で API を実行する場合は、 `{firebase プロジェクトディレクトリ}/functions` ディレクトリ内でコンフィグ用ファイルを作成してから動作を確認しました。

```
$ firebase functions:config:get > .runtimeconfig.json
$ firebase serve --only functions
```

### API 処理の実装

#### API エンドポイント処理

今回はやりたかったことの要件を満たしつつユースケースをAPI 処理を実装していきます。

ミドルウェアの使用

今回は、API エンドポイント処理を単体テスト可能にするため、Firebase SDK の Cloud Function エンドポイント処理用のメソッドのコールバック処理内でミドルウェアを指定しています。
パラメータに [Express.js](http://expressjs.com/ja/) のインスタンスを指定する方が簡潔なコードになるのですが、単体テストの書き方がわかりませんでした :sob:

``` index.js
const cors = require('cors')({origin:true})
const compression = require('compression')()
...
exports.topics = functions.https.onRequest((request, response) => {
    cors(request, response, () => {
        compression(request, response, () => {
            // API ビジネスロジック
            ...
```

Twitter API との連携

ツイートデータのキャッシュ
Twiiter API のリクエストレートリミット対策をしたかったので、ツイートデータを [memory-cache](https://www.npmjs.com/package/memory-cache) モジュールでキャッシュするようにしました。

```
$ npm install --save memory-cache
```

ユニットテストで Firebase 環境変数を使う

CI 環境で実行するユニットテストで Firebase 環境設定を取得する方法がわからなかったので、設定取得処理をラップしてモックできるようにしました。

``` index.js
const configModule = new ConfigModule(functions.config())
```

## ユニットテスト

テスト用のフレームワークは [Jest](https://jestjs.io/ja/) を使いました。
`{Firebase プロジェクト}/functions` ディレクトリでインストールコマンドを実行したらテストが実行可能になります。

```
$ npm install --save-dev jest
```

API やモジュールのテストケースを実装したら実行します。

```
$ npm test
```

<img width="395" alt="スクリーンショット 2018-07-09 15.27.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/13105/fca209d7-6a07-fdd4-f026-09a15c74c8d1.png">

## インテグレーションテスト

今回はユニットテストでは環境設定の取得やモジュールをモックしているので、実際の設定を使用した動作確認はインテグレーションテストのスコープに入れました。CI 環境での自動テストは未設定のため手動で実行しています。

ローカルサーバ起動コマンド

```
$ firebase serve --only functions
```


`http://localhost:5000/{プロジェクトID}/{リージョン}/topics` にアクセスして Tweet データが表示されるか確認します。

ローカルサーバで API の動作が確認できたら、[Vue.js で作ったフィードリーダー](https://qiita.com/tanakaoriginal/items/2d6c3942dc07b8dd6e37) の E2E テストでも動作を確認しています。

## QA テスト

QA 用の Firebase プロジェクトにデプロイして動作確認をします。

```
$ firebase deploy --only functions
```

`https://{QA用プロジェクトの公開用サブドメイン}.cloudfunctions.net/topics` にアクセスしてローカルテスト同様の Tweet データが表示されるか確認します。インテグレーションテストと同様にクライアントとの接続テストも確認してみます。

## 参考にさせていただいた記事

### 環境設定関連

* [Firebase - プロジェクトの環境設定を設定する](https://firebase.google.com/docs/functions/config-env?hl=ja#set_environment_configuration_for_your_project)
* [Firebase - 環境の構成](https://firebase.google.com/docs/functions/config-env?hl=ja)
* [Firebase - キャッシュ動作を管理する](https://firebase.google.com/docs/hosting/functions?hl=ja#manage_cache_behavior)
* [Google Cloud Functions で環境変数を使うには（Tipsネタも） ](https://www.totalsolution.biz/google-cloud-functions-env/)
* [Cloud Functions シェルをインストールして設定する](https://firebase.google.com/docs/functions/local-emulator?hl=ja#install_and_configure_the_cloud_functions_shell)

* [Firebaseで動かすNode.jsアプリ入門🔥](https://qiita.com/seya/items/225f859d775b31047000)
* [HTTP リクエスト経由で関数を呼び出す](https://firebase.google.com/docs/functions/http-events?hl=ja)
* [Cloud Functions for Firebaseとは？](https://qiita.com/koki_cheese/items/013d4e6ab5aefc792388)

### キャッシュ関連

* [A simple in-memory cache for node.js](https://www.npmjs.com/package/memory-cache)
* [Simple server side cache for Express.js with Node.js](https://medium.com/the-node-js-collection/simple-server-side-cache-for-express-js-with-node-js-45ff296ca0f0)