目次

はじめに
執筆の経緯
本書の流れ
表記関係について
第1章 環境構築
1.1 プロジェクト作成
1.2 Lint の設定
1.3 Firebaseのインストール
1.4 各種設定
第2章 モデリング
2.1 型の定義
2.2 セキュリティールールテストの準備
2.3 userのセキュリティールールテスト
2.4 message のセキュリティールールテスト
2.5 Emulatorを使ったカバレッジの確認
2.6 CIの設定
第3章 認証機能
3.1 ログインプロバイダの有効化
3.2 ライブラリーのインストール
3.3 AuthContextオブジェクトの作成
3.4 AuthProviderの作成
3.5 useAuthフックの作成
3.6 AuthContextテストの作成
3.7 認証関数の作成
3.8 ログイン画面の追加
3.9 ローディング画面の追加
第4章 チャット(メッセージ表示)
4.1 画面表示の確認
4.2 メッセージ部分の作成
4.3 ユーザー情報へのアクセス方法
4.4 Messageコンポーネントの作成
第5章 チャット(メッセージ一覧)
5.1 クエリの作成とテスト
5.2 Messagesコンポーネントの作成
第6章 チャット(入力フォーム)
6.1 MessageForm テストの作成
6.2 データ登録処理の追加
6.3 MessageForm コンポーネントの作成
6.4 MessageForm テストの修正
6.5 動作確認
第7章 チャット(画像添付)
7.1 Storageセキュリティールールの作成とテスト
7.2 投稿に添付画像を追加
7.3 画像の登録
7.4 動作確認
第8章 プッシュ通知
8.1 ウェブプッシュ証明書の生成
8.2 userSecretsコレクションの追加
8.3 Service Workerの作成
8.4 トークン登録の実装
8.5 トークン登録テストの追加
8.6 Firebase Functionsの環境構築
8.7 型定義の共有化
8.8 メッセージ通知機能の実装
8.9 メッセージ通知機能のテスト
8.10 投稿者への不要な通知をなくす
8.11 CIの設定
第9章 E2Eテスト
9.1 Cypressのインストール
9.2 Emulatorの準備
9.3 CypressとEmulatorの接続
9.4 テスト用初期データの作成
9.5 Specファイルの修正
9.6 テストの実行
9.7 CIの設定
終わりに

はじめに

 本書は、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な開発を行っています。

TypeScript を使う

 Firestore並びにRealtime Databaseは、NoSQLでどんな形のデータでも保存できます。それゆえ、TypeScriptを使っていない場合、どういったデータが入っているかを継続的に把握するのが困難です。TypeScriptはかなり柔軟な型表現ができるため、NoSQLを安全に取り扱うのに非常に適しています。

CIで動くテストを書く

 個人個人が手元でチェックするだけではテストの実行を忘れ、テストが壊れていることさえ気づかないケースがあり、テストを書いたことに安心していては逆に危険なときもあります。

 全てのテストが常に想定通り動いていることを把握するためにも、CIは必ず設定するようにしています。

 最近ではEmulatorが進化し、ほとんどのケースでテストが動く状態なので、CIで動かすときはEmulatorを使うようにしています。

セキュリティールールのテストのカバレッジを100%に

 セキュリティールールは、安全にFirestore / Storageを使う上で非常に大事な機能です。重大なセキュリティー事故を引き起こさないためにも、テストケースをなるべく網羅した形でテストは書いていくべきだと考えています。

セキュリティールール以外はカバレッジを重要視しない

 重要、もしくは複雑なロジック(金額計算など)については、積極的にテストを使って品質を担保していくべきです。ただ、ユニットテストを行う領域は、変更に弱いことも多く、保守コストがかかりすぎるのでカバレッジ100%を目指す必要はないと考えています。

 一方でコンポーネントのテストについては、ライブラリー更新時のエラー等も検出できるように、単純でもいいので、全てのコンポーネントが1度は動くようにテストを書くようにしています。

 Firebase Functionsは、基本的に重要な処理を記載していることが多いので、全ての関数のテストを書いています。

E2Eテストは頑張りすぎない

 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"

表記関係について

 本書に記載されている会社名、製品名などは、一般に各社の登録商標または商標、商品名です。会社名、製品名については、本文中では©、®、™マークなどは表示していません。

第1章 環境構築

 この章では、これから作成していくサンプルアプリケーションの下準備として、環境構築を行っていきます。この章ではプロジェクトの作成から順に行っていきますが、最終的にFirebase Hosting環境にアプリをデプロイすることをゴールとします。

1.1 プロジェクト作成

 まずはじめに、プロジェクトを作成します。create-viteコマンドを利用しますが、事前にインストールする必要はなく、下記のコマンドを実行するとインストールからプロジェクトの作成まで一括して行います。また、ReactとTypeScriptを利用するので、react-tsテンプレートを指定しています。

yarn create vite --template react-ts

 プロジェクト名は自由に設定して構いませんが、ここではtestable-firebase-sample-chatとして進めていきます。

図1.1: Viteによる新規プロジェクトの作成

 以下のように、ディレクトリー配下にソースが生成されます。

図1.2: プロジェクト作成後のディレクトリー状況

 作成されたプロジェクトはnode_modulesがまだない状態なので、作成しましょう。

cd testable-firebase-sample-chat
yarn install

 yarn installが完了したら、yarn devでとりあえず動くようになっています。

図1.3: 開発環境で動作したApp.tsxの画面

1.2 Lint の設定

 次に、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
図1.4: 対話形式でeslntの設定を行っていく

 インストールが終わり、.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: {},
};

1.3 Firebaseのインストール

Firebaseプロジェクトの作成

 Firebaseをアプリに組み込んでいきますが、まずはFirebaseプロジェクトを作成する必要があります。

 Firebaseコンソールにアクセスして、新規プロジェクトを作成しましょう。

 https://console.firebase.google.com

 プロジェクト名は任意ですが、ここでもtestable-firebase-sample-chatで進めていきたいと思います。

図1.5: Firebaseプロジェクトの新規作成

 今回Firebase Functionsを利用しますが、これはBlazeプラン(有料プラン)でしか利用できません。
プロジェクトを作成したら、プランをBlazeに上げましょう。

Firebase JavaScript SDKのインストール

 次に、Firebase JavaScript SDKを追加していきましょう。また、ローカルでfirebaseコマンドを利用するためのfirebase-toolsも追加します。firebase-toolsはプロジェクトを横断して利用することが多いので、グローバルにインストールするといいでしょう。

yarn add firebase
yarn global add firebase-tools

 インストールが完了したら、firebase initコマンドで初期化を行います。

firebase init
図1.6: Firebaseプロジェクトの紐付け

 ここでも対話形式で進むため、以下の選択をしていきます。

 ・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

 .firebasercfirebase.jsonなどの設定ファイルが生成されたら完了です。

1.4 各種設定

import時のシンボル設定

 モジュール等をimportする際、相対パスではなくシンボルを利用した絶対パスを使えるようにします。

tsconfig.json

 compilerOptionsbaseUrlpathsを追加します。

  "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.config.ts

 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することができるようになりました。

firebase.tsの作成

 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環境で、アプリの動作が確認できたら作業は完了です。

試し読みはここまでです。
この続きは、製品版でお楽しみください。