本書は、Firebaseを使ったウェブアプリケーションをTestableな形で開発していくための技術解説書です。
フレームワークとしてはVite/Reactを利用し、テストフレームワークとしてはVitest、Firebaseもウェブアプリケーションとしては一般的なFirebase Authentication、Firestore、Firebase Functions、Firebase Storageを対象としています。
冒頭で開発する流れを説明した上で、サンプルコードを使ってなるべく具体的な例でテストを書きながら開発が進めていけるような形でまとめています。ご自身のユースケースのところだけを読んでいただいても理解できるようになっています。
私たちが所属しているSonicGardenは、主にRuby on Railsを利用して開発を行ってきました。そんな中でFirebaseが登場し、自社サービスや受託案件などでも利用されるようになってきています。
SonicGardenではソフトウェアは変化し続けるものと定義しており、ReadableでTestableなコーディングを非常に大事にしています。
Ruby on Railsでは主にRSpecを使うSonicGarden流の書き方がまとまっていますが、Firebaseはまだまだ新しいサービスなので、絶賛模索中という状況でした。
現在のFirebaseは、やっとFirebase StorageのEmulatorが登場し、基本的な全てのサービスをEmulatorで動かすことができ、テストのしやすい環境ができあがりつつあります。そこで見えてきた我々が考える、今できる最大限のTestableな開発を言語化しようということで、本を書いてまとめようということになりました。
本書では各章がある程度の機能単位でテストと開発を進めて、ひとつのサンプルアプリケーションを作成することを目的としています。
作成するサンプルアプリケーションは、以下の仕様になります。
ReactのSPAでチャットアプリを作成します。
・Googleログインによるログイン機能を持つ
・入力フォームからチャットに投稿ができる
・チャットには文字と画像を投稿できる
・チャットには投稿者のプロフィール画像、名前、投稿時間が表示される
・投稿されたチャットはタイムラインに上から順に表示される
・チャット投稿時にプッシュ通知が送信される
本書では、テストが全体的に揃っている状態を目指して、実装とテスト、両方を少しずつ加えていく流れで書いています。これは、実際のプロダクト開発で私達が行っている開発スタイルそのものでもあります。
私たちは、以下の方針でTestableな開発を行っています。
Firestore並びにRealtime Databaseは、NoSQLでどんな形のデータでも保存できます。それゆえ、TypeScriptを使っていない場合、どういったデータが入っているかを継続的に把握するのが困難です。TypeScriptはかなり柔軟な型表現ができるため、NoSQLを安全に取り扱うのに非常に適しています。
個人個人が手元でチェックするだけではテストの実行を忘れ、テストが壊れていることさえ気づかないケースがあり、テストを書いたことに安心していては逆に危険なときもあります。
全てのテストが常に想定通り動いていることを把握するためにも、CIは必ず設定するようにしています。
最近ではEmulatorが進化し、ほとんどのケースでテストが動く状態なので、CIで動かすときはEmulatorを使うようにしています。
セキュリティールールは、安全にFirestore / Storageを使う上で非常に大事な機能です。重大なセキュリティー事故を引き起こさないためにも、テストケースをなるべく網羅した形でテストは書いていくべきだと考えています。
重要、もしくは複雑なロジック(金額計算など)については、積極的にテストを使って品質を担保していくべきです。ただ、ユニットテストを行う領域は、変更に弱いことも多く、保守コストがかかりすぎるのでカバレッジ100%を目指す必要はないと考えています。
一方でコンポーネントのテストについては、ライブラリー更新時のエラー等も検出できるように、単純でもいいので、全てのコンポーネントが1度は動くようにテストを書くようにしています。
Firebase Functionsは、基本的に重要な処理を記載していることが多いので、全ての関数のテストを書いています。
E2Eテストは、アプリケーションをユーザー目線で実行する重要なテストです。ただし、ここについても完璧を求めすぎると、いくら時間があっても足りない状況になると思います。なので、各画面のシンプルな操作のテストにとどめ、複雑なケースについては他のテストで担保するといいと思っています。
本書で説明するサンプルコードは、以下のリポジトリーにて公開しています。
https://github.com/SonicGarden/testable-firebase-sample-chat
クライアントで利用しているフレームワーク、ライブラリーは以下の通りです。
"date-fns": "^2.28.0",
"firebase": "^9.9.0",
"lodash-es": "^4.17.21",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-firebase-hooks": "^5.0.3"
"@firebase/rules-unit-testing": "^2.0.3",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.3.0",
"@types/lodash-es": "^4.17.6",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"@vitejs/plugin-react": "^1.3.0",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-react": "^7.30.1",
"fishery": "^2.2.2",
"jsdom": "^20.0.0",
"prettier": "^2.7.1",
"typescript": "^4.6.3",
"vite": "^2.9.9",
"vite-tsconfig-paths": "^3.5.0",
"vitest": "^0.18.0"
Functionsで利用しているフレームワーク、ライブラリーは以下の通りです。
"firebase-admin": "^11.0.1",
"firebase-functions": "^3.18.0"
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"eslint": "^8.9.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.25.4",
"firebase-functions-test": "^2.2.0",
"prettier": "^2.7.1",
"typescript": "^4.5.4",
"vitest": "^0.21.0"
本書に記載されている会社名、製品名などは、一般に各社の登録商標または商標、商品名です。会社名、製品名については、本文中では©、®、™マークなどは表示していません。
この章では、これから作成していくサンプルアプリケーションの下準備として、環境構築を行っていきます。この章ではプロジェクトの作成から順に行っていきますが、最終的にFirebase Hosting環境にアプリをデプロイすることをゴールとします。
まずはじめに、プロジェクトを作成します。create-viteコマンドを利用しますが、事前にインストールする必要はなく、下記のコマンドを実行するとインストールからプロジェクトの作成まで一括して行います。また、ReactとTypeScriptを利用するので、react-tsテンプレートを指定しています。
yarn create vite --template react-ts
プロジェクト名は自由に設定して構いませんが、ここではtestable-firebase-sample-chatとして進めていきます。
以下のように、ディレクトリー配下にソースが生成されます。
作成されたプロジェクトはnode_modulesがまだない状態なので、作成しましょう。
cd testable-firebase-sample-chat
yarn install
yarn installが完了したら、yarn devでとりあえず動くようになっています。
次に、Lintの設定をしていきます。
EslintとPrettierをインストールします。
yarn add --dev eslint prettier eslint-config-prettier
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@vitejs/plugin-react": "^1.3.0",
+ "eslint": "^8.19.0",
+ "eslint-config-prettier": "^8.5.0",
+ "prettier": "^2.7.1",
"typescript": "^4.6.3",
"vite": "^2.9.9"
Eslintをインストールしただけでは設定がされていないため、以下のコマンドで設定ファイルを作成していきます。
yarn create @eslint/config
コンソール上で設定に関する質問がされるので、以下のように指定していきます。
・How would you like to use ESLint?
─To check syntax and find problems
・What type of modules does your project use?
─JavaScript modules (import/export)
・Which framework does your project use?
─React
・Does your project use TypeScript?
─Yes
・Where does your code run?
─Browser
・What format do you want your config file to be in?
─JavaScript
・The config that you've selected requires the following dependencies:
eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
Would you like to install them now?
─Yes
・Which package manager do you want to use?
─yarn
最後に、以下のライブラリーをインストールするか聞かれますので、インストールしましょう。
eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest
@typescript-eslint/parser@latest
インストールが終わり、.eslintrc.cjsファイルが作成されれば完了です。
// .eslintrc.cjs
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint'],
rules: {},
};
Firebaseをアプリに組み込んでいきますが、まずはFirebaseプロジェクトを作成する必要があります。
Firebaseコンソールにアクセスして、新規プロジェクトを作成しましょう。
https://console.firebase.google.com
プロジェクト名は任意ですが、ここでもtestable-firebase-sample-chatで進めていきたいと思います。
今回Firebase Functionsを利用しますが、これはBlazeプラン(有料プラン)でしか利用できません。
プロジェクトを作成したら、プランをBlazeに上げましょう。
次に、Firebase JavaScript SDKを追加していきましょう。また、ローカルでfirebaseコマンドを利用するためのfirebase-toolsも追加します。firebase-toolsはプロジェクトを横断して利用することが多いので、グローバルにインストールするといいでしょう。
yarn add firebase
yarn global add firebase-tools
インストールが完了したら、firebase initコマンドで初期化を行います。
firebase init
ここでも対話形式で進むため、以下の選択をしていきます。
・Which Firebase features do you want to set up for this directory? Press Space to select features, then Enter to confirm your choices.
─Firestore
─Functions
─Hosting
─Storage
─Emulator
・Please select an option:
─Use an existing project
─先程作成したFirebaseプロジェクトを指定します。
・What file should be used for Firestore Rules?
─firestore.rules
・What file should be used for Firestore indexes?
─firestore.indexes.json
・What language would you like to use to write Cloud Functions?
─TypeScript
・Do you want to use ESLint to catch probable bugs and enforce style?
─Yes
・Do you want to install dependencies with npm now?
─Yes
・What do you want to use as your public directory?
─dist
・Configure as a single-page app (rewrite all urls to /index.html)?
─Yes
・Set up automatic builds and deploys with GitHub?
─No
.firebasercやfirebase.jsonなどの設定ファイルが生成されたら完了です。
モジュール等をimportする際、相対パスではなくシンボルを利用した絶対パスを使えるようにします。
compilerOptionsにbaseUrlとpathsを追加します。
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
+ "baseUrl": "src",
+ "paths": {
+ "@/*": ["*"]
+ }
},
vite-tsconfig-pathsを使用して、シンボルでのパス解決をできるようにします。vite-tsconfig-pathsを追加して、vite.config.tsに設定します。
yarn add vite-tsconfig-paths
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+ import tsconfigPaths from 'vite-tsconfig-paths';
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), tsconfigPaths()],
})
これでsrcの代わりに@を指定することで、絶対パスでimportすることができるようになりました。
src/lib配下に、Firebaseへの接続情報などを格納するファイルfirebase.tsを作成します。
ここでは接続情報自体は.envファイルに記載し、Viteの環境変数を通じて設定するようにしています(Firebaseの接続情報は公開して問題ないものなので、必ずしも環境変数で秘匿する必要はありませんが、ここでは環境の切り替えを容易にするなどの目的で.envを利用しています)。
// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env
.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
};
initializeApp(firebaseConfig);
main.tsxで上記ファイルをimportします。先程設定した@を使うことで、絶対パスによるimportができていますね。
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
+ import '@/lib/firebase';
ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
package.jsonのスクリプトにdeployコマンドを追記しましょう。これでデプロイするとき、コマンドひとつでできるようになります。
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
+ "deploy": "yarn build && firebase deploy --except functions"
},
追記したら実際に叩いてみます。
yarn deploy
実際のFirebase Hosting環境で、アプリの動作が確認できたら作業は完了です。