🔥

Cloud Firestore運用ツールfsrplを使ってテストデータを用意する

2020/11/02に公開
2

これは何

Firebase(具体的には、Cloud Firestore)の運用に便利なfsrpl (Firestore Replication Tool) と、その使い方について紹介します。以前、別の場所に投稿したものがありますが、このCLIツールにオプション変更のあったため本記事では最新の利用方法についてユースケースを添えて紹介します。

fsrplの概要

FirebaseOpenSourceでも紹介されているCLIツールで、下記の3つの機能があります。(READMEからの抜粋です)

  1. copy 特定のドキュメントを、別のCollection配下にコピーできる。また、ワイルドカードを利用することでコレクション配下のすべてのドキュメントをコピーできる。
    さらに、特定のProjectのFirebaseから他のFirebaseへ、特定のドキュメントのデータをコピーできる
  2. dump 特定のドキュメントをローカルのJSONファイルとしてバックアップができる。
  3. restore ローカルのJSONファイルからドキュメントを復元できる。firestore emulatorへもデータの復元ができるため、テストデータ作成にも利用できる

fsrplのインストール

公式のREADME通りですが、macユーザーであればbrewでインストールが可能です。

brew tap matsu0228/homebrew-fsrpl
brew install fsrpl

他にも go getによる方法や、バイナリインストールによる方法があります。

ユースケース

これらの機能について、利用できるシーンはいくつもありそうですが、実際のユースケースを考えてみました。

  1. copy 開発環境のFirestoreに設定したデータを、本番環境へコピーする。
  2. dump Firestoreの特定のドキュメントをエクスポートしてテストデータとして利用する。
  3. restore dumpでエクスポート済みのデータをFirestore Emulatorへインポートしてテストに用いる。

開発環境から、本番環境へコピーする

「開発環境のFirestoreに本番公開用のデータを用意し、動作試験を行う。データに問題ないことが確認できたら本番のFirestoreにコピーする」といったユースケースがあります。そういった場合にもfsrplが利用できます。

copyコマンドのhelpを見る
fsrpl copy -h
Usage: fsrpl copy --dest=STRING <firestore_path>

Copy data from specific firestore path to another firestore path.

Arguments:
  <firestore_path>    Source firestore path. The path contains colleaction's path and document id. Document id allowed
                      wildcard character (*). (e.g. collectionName/docId, collectionName/*)

Flags:
  -h, --help                Show context-sensitive help.
      --debug               Enable debug mode.

      --cred=STRING         Set source firestore's credential file path.
      --dest=STRING         Destination firestore path. The path contains colleaction's path and document id. Document
                            id allowed wildcard character (*). (e.g. collectionName/docId, collecntionName/*)
      --dest_cred=STRING    Set destination firestore's credential file path.
      --is-delete           delete source document data after dump.

上記のhelp通りなのですが、下記のようにすることで開発環境から本番環境へのデータコピーが可能です。

fsrpl copy --cred **開発環境のFirestoreのcredentialJSON**  \
  --dest_cred **本番環境のFirestoreのcredentialJSON** \
  --dest **コレクション+ドキュメント** \
  **コレクション+ドキュメント**


# 実例1: dev.jsonのFirestoreからprod.jsonへ、「products/banana」のドキュメントだけをコピーする例
fsrpl copy --cred ./secrets/dev.json  \
  --dest_cred ./secrets/prod.json \
  --dest "products/banana" \
  "products/banana"

# 実例2: ドキュメントIdにワイルドカード(*)を指定することで、全てのドキュメントを対象とする
fsrpl copy --cred ./secrets/dev.json  \
  --dest_cred ./secrets/prod.json \
  --dest "products/*" \
  "products/*"

また、このツールではドキュメントIdは任意のものを指定できますが、複数のドキュメント配下のコレクションパスは指定できません。これは、たとえば、user/{userId}/favoriteProductsコレクション配下を全てコピーといったケースです。
こういったケースでは、簡単なshellスクリプトを書くことでこれを実現できます。

#!/bin/bash

#このdocumentIdは、後述するdumpコマンドで users/配下の全ドキュメントIdを控えておいて列挙する
users=("gorilla" "duck" "tiger") 

for userId in ${users[@]} ; do
  fsrpl copy --cred ./secrets/dev.json  \
    --dest_cred ./secrets/prod.json \
    --dest "users/${userId}/favoriteProducts/*" \
    "users/${userId}/favoriteProducts/*"
done

Firestoreの特定のドキュメントをエクスポートしてテストデータとして利用する

実データから[1]、手軽にテストパターンを列挙してユニットテストを書くといった場合に便利です。
また、すでにあるテストケースに、不具合が発生したパターンを追加するといった場合にもすぐ対応できますし、テストデータをGit管理することで、チーム間/CIでのテストパターンも簡単に用意できます。

dumpコマンドのhelpを見る
fsrpl dump -h

Usage: fsrpl dump <firestore_path>

Dump data to json files.

Arguments:
  <firestore_path>    Target firestore path. The path contains colleaction's path and document id. Document id allowed wildcard character (*). (e.g. collectionName/docId, collecntionName/*)

Flags:
  -h, --help              Show context-sensitive help.
      --debug             Enable debug mode.

      --cred=STRING       Set target firestore's credential file path.
      --path="./"         Export local path.
      --show-go-struct    Show go struct mode without json file exportation.

上記のhelp通りなのですが、下記のようにすることでFirestoreのドキュメントをエクスポートできます。

fsrpl dump --cred **開発環境のFirestoreのcredentialJSON**  \
  --path **json格納先のローカルディレクトリ**
  **コレクション+ドキュメント**

# 実例: 「products/*」のドキュメントを一括でdumpする
fsrpl dump --cred ./secrets/dev.json  \
  --path "./__tests__/testData"
  "products/*"

上記のテストデータをユニットテストで利用する例を示します。(Jestの例ですが、他の言語でも活用できます)


import * as fs from "fs";

export const importTestData = (pathStr: string): { [fn: string]: any } => {
  const json: { [fn: string]: any } = {};
  const dir = `${__dirname}${pathStr}`;
  const paths = fs.readdirSync(dir);
  paths.forEach((fn) => {
    try {
      const buffer = fs.readFileSync(`${dir}/${fn}`, "utf8");
      json[fn.split(".")[0]] = JSON.parse(buffer);
    } catch (error) {
      console.log(`failed to read ${error} with: `, dir, fn);
    }
  });
  return json;
};

// ↑ helper.ts とかに切り出すと使いやすい

describe("sumProducts()", () => {
  const testData = importTestData("/testData") as { [fn: string]: Product };
  const { banana, apple, grape } = testData;
  test.each([
    [[banana], 10000],
    [[banana, apple], 10500],
    [[apple, grape], 1300],
  ])(
    "sumProducts(): sum %o prices to equal %p",
    async (products, expected) => {
      expect.assertions(1);
      const actual = await sumProducts(products);
      expect(actual).toBe(expected);
    }
  );
});

エクスポート済みのデータをFirestore Emulatorへインポートしてテストに用いる

Emulatorをもちいた試験でも、fsrplが有用です。ローカルにdump済みのJSONファイルを指定してEmulatorへ復元できます。

restoreコマンドのhelpを見る
fsrpl restore -h


Usage: fsrpl restore --path=STRING <firestore_path>

Restore data from json files.

Arguments:
  <firestore_path>    The destination firestore path to restore to. The path contains colleaction's path and document
                      id. Document id allowed wildcard character (*). (e.g. collectionName/docId, collecntionName/*)

Flags:
  -h, --help                           Show context-sensitive help.
      --debug                          Enable debug mode.

      --path=STRING                    The path to the local file containing the data to be restored.
      --cred=STRING                    Set target firestore's credentail file path.
      --emulators-project-id=STRING    Set projectID of firestore emulator.

上記のhelp通りなのですが、下記のように復元ができます。

FIRESTORE_EMULATOR_HOST=**Firestore EmulatorのURL**  fsrpl restore \
  --path **インポート対象のJSON or ディレクトリを指定**
  --emulators-project-id **emulatorのprojectID** \
  **コレクション+ドキュメント**

# 実例1: EmulatorURL=localhost:58080, testData/products/banana.jsonの復元
# 事前にエミュレーターを起動しておく: firebase emulators:start --only firestore
FIRESTORE_EMULATOR_HOST=localhost:58080  fsrpl restore \
  --path "./testData/banana"
  --emulators-project-id emulator-test \
  "products/products/banana"

# 実例2: testData/products配下のすべてのデータを復元
FIRESTORE_EMULATOR_HOST=localhost:58080  fsrpl restore \
  --path "./testData/products/"
  --emulators-project-id emulator-test \
  "products/*"
import { execSync } from "child_process";

export const importToEmulator = async (
  fsPath: string,
  collectionPath: string,
  projectId: string
): Promise<void> => {
  const restoreCmd = `FIRESTORE_EMULATOR_HOST=${emulatorHost} fsrpl restore --path "${fsPath}" "${collectionPath}/*" --debug --emulators-project-id=${projectId}`;
  console.log(`restore: ${restoreCmd}`);
  const stdout = await execSync(`${restoreCmd}`);
  console.log(`imported test data: ${stdout.toString()}`);
};

// ↑ helper.tsとかに切り出しておくと使いやすい

import * as helper from "./helper";
import * as firebase from "@firebase/rules-unit-testing";

describe("products", () => {
  const projectId = "emulator-test";
  const testDB = firebase.initializeAdminApp({ projectId }).firestore();
  
  beforeAll(async () => {
    await importToEmulator(
      "./src/__test__/testData/products/",
      "products",
      projectId
    );
  });

  afterAll(async () => {
    await firebase.clearFirestoreData({
      projectId,
    });
    await Promise.all(firebase.apps().map((app) => app.delete()));
    console.log("cleanup test data: ", projectId);
  });

  test.each([
  // emulator接続のテストパターンを列挙
  ])(
    "calculatePrices(): sum %o prices to equal %p",
    async (productIds, expected) => {
      expect.assertions(1);
      const actual = await fetchDataFromFirestore() // emulator接続を含むテストコード
      expect(actual).toBe(expected);
    }
  );
});

最後に

fsrplを使ってみての使いにくい点、機能要望などがあればお気軽にコメント/Tweet(@mtskhs)ください🙏

おしらせ

2020/12/26〜の技術書典10にて、「Firestore Testing - なぜテストを書くのか、どう書くのかがよくわかる」という書籍を発売予定です。お手にとっていただけると嬉しいです!

👉👉 サークルページ 👈👈

脚注
  1. 商用データをテストに用いる場合には、個人情報などを含めないように注意しましょう。たとえば、ローカルにエクスポートした際に、個人情報に関わるデータは別の文字列で上書きするなど。 ↩︎

Discussion

amukateramukater

dump の実例のコマンドが copy になっていませんか?

mtskhsmtskhs

ご指摘頂きありがとうございます!修正しました。