Cloud Firestore運用ツールfsrplを使ってテストデータを用意する
これは何
Firebase(具体的には、Cloud Firestore)の運用に便利なfsrpl (Firestore Replication Tool) と、その使い方について紹介します。以前、別の場所に投稿したものがありますが、このCLIツールにオプション変更のあったため本記事では最新の利用方法についてユースケースを添えて紹介します。
fsrplの概要
FirebaseOpenSourceでも紹介されているCLIツールで、下記の3つの機能があります。(READMEからの抜粋です)
-
copy
特定のドキュメントを、別のCollection配下にコピーできる。また、ワイルドカードを利用することでコレクション配下のすべてのドキュメントをコピーできる。
さらに、特定のProjectのFirebaseから他のFirebaseへ、特定のドキュメントのデータをコピーできる -
dump
特定のドキュメントをローカルのJSONファイルとしてバックアップができる。 -
restore
ローカルのJSONファイルからドキュメントを復元できる。firestore emulatorへもデータの復元ができるため、テストデータ作成にも利用できる
fsrplのインストール
公式のREADME通りですが、macユーザーであればbrew
でインストールが可能です。
brew tap matsu0228/homebrew-fsrpl
brew install fsrpl
他にも go get
による方法や、バイナリインストールによる方法があります。
ユースケース
これらの機能について、利用できるシーンはいくつもありそうですが、実際のユースケースを考えてみました。
-
copy
開発環境のFirestoreに設定したデータを、本番環境へコピーする。 -
dump
Firestoreの特定のドキュメントをエクスポートしてテストデータとして利用する。 -
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 - なぜテストを書くのか、どう書くのかがよくわかる」という書籍を発売予定です。お手にとっていただけると嬉しいです!
👉👉 サークルページ 👈👈
-
商用データをテストに用いる場合には、個人情報などを含めないように注意しましょう。たとえば、ローカルにエクスポートした際に、個人情報に関わるデータは別の文字列で上書きするなど。 ↩︎
Discussion
dump の実例のコマンドが copy になっていませんか?
ご指摘頂きありがとうございます!修正しました。