Vue + Firebaseで すでに稼働していて、テストがないシステムにどのように テストを追加して、リファクタリングをすすめていくか検討しました。 UnitTestリファクタリングの前段階として Storybook,reg-suitを導入してVisualRegressionTestを まず実現したというところをまとめます。
WebAppのUnitTestについて一般論
下記のスライドを参考にしました。
ポイント
Web Componentのテストといったときに下記のような入力、出力があるが
そのなかで Props, Vuex,State → component → HTML/CSSをテストするのがVRTです。
-
入力
- Lifecycle
- Props
- Vuex,State
- UserInteration
-
出力
- HTML/CSS
- Event
- VuexAction
導入作戦
これからプロジェクトをはじめる場合は WebUI要素ごとに vue componentを作成し、ひとつづつStorybookに登録して確認をとりながらすすめるのがまっとうなやり方だと思います。
でも、すでに動いているVueプロジェクトで、かつあまりComonent化されておらず Pageにドバっと数千行のコードが書かれているとしたら、、、 リファクタリングするにしてもUnitTestもないので怖くて手がつけられない。 なので戦略としてはVueのPageをそのままStorybookに登録して まずVRTを主要ページ分確立したいと思います。 その後リファクタリングをおこない VueComponentへ切り出しをすすめるというやり方を採用していきたいです。 Karma, Jest等の導入もしたいがまず面でとらえるVRTを導入するほうが費用対効果高いと考えました。
storybook, storycap, reg-suitの組込み
シンプルにVueでstorybookを動かしたい場合、Libのインストールは、下記などを参考に普通にnpm(yarn) installするだけです。
storybook, storycap, reg-suit系のコマンドは storybookの起動に時間がかかります。同じsrcで何回かscreenshotを試したいことがあったので、下記のように screenshot-built を追加しました。
"scripts": {
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook -o dist-storybook",
"screenshot": "storycap --serverCmd \"start-storybook -p 6006\" http://localhost:6006 --serverTimeout 600000 --delay 1000 --verbose true",
"screenshot-built": "storycap --serverCmd \"npx http-server dist-storybook -p 6006\" http://localhost:6006 --serverTimeout 600000 --delay 1000 --verbose true",
"shot": "storycap http://localhost:6006 --serverTimeout 600000",
"regression": "reg-suit run"
}
Screenshotのオプション説明
- --serverTimeout 600000: ページ全体なのでロードに時間かかるケースがあったため
- --delay 1000: load後、shotをとるまえに1秒ほどまってくれる
- --verbose true: github actionなどで実行したときに情報多く出力したい場合はtrue(通信logが沢山はかれる)
Storycapをmanagedなモードでstorybookに組み込む(storycapの都合にあわせてstorybookを制御してくれる)ためにstorybookのmain.jsに下記のようにaddonとしてくみこみます。
module.exports = {
"stories": [
"../*/*.stories.mdx",
"../*/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
'storycap'
],
storybook において test data どう設定するか問題
対象Projectは主要データをFirebase、Firestoreに保存しており 開発に応じて随時データも更新されていく。
VRTは基本的に画面のスクショの差分を管理するテスト手法なので データが変わってしまうと差分が多くなりすぎてコードの問題なのか、データが変わっただけなのかが判定できず意味をなさなくなる。
FirebaseをMock化することも考慮したが、本家Google様が出しているFirebaseEmulatorをいれることがスマートだと考えました。
Firebase Emulator 導入
まずローカルにemulatorをInstallする
emulatorは node, javaで動いているので事前にインストールしておく必要があります。
mac, ubuntu等ではdefaultでInstallされています。
% java -version
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)
% node --version
v16.13.2
% npm install -g firebase-tools
% firebase login
Already logged in as {$USER}
- 初期構築する場合は、
firebase init emulators
を実行します - firestore, storageのほかに、テスト用のユーザーも同じIDで使いまわしたいので Authenticationもemulateしておく必要があります
以下のログが出力されれば成功です。Authentication,firestore, storageがLocalのそれぞれのportでアクセスできるようになります。
(base) firebase_emulator % npx firebase emulators:start -
i emulators: Starting emulators: auth, firestore, storage
i firestore: Firestore Emulator logging to firestore-debug.log
i auth: Importing config from /XXX/firebase_emulator/exported20220227/auth_export/config.json
i auth: Importing accounts from /XXX/firebase_emulator/exported20220227/auth_export/accounts.json
i ui: Emulator UI logging to ui-debug.log
┌─────────────────────────────────────────────────────────────┐
│ ✔ All emulators ready! It is now safe to connect your app. │
│ i View Emulator UI at http://localhost:4000 │
└─────────────────────────────────────────────────────────────┘
┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator │ Host:Port │ View in Emulator UI │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ localhost:8090 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Storage │ localhost:9199 │ http://localhost:4000/storage │
└────────────────┴────────────────┴─────────────────────────────────┘
Emulator Hub running at localhost:4400
Other reserved ports: 4500
データ追加、参照
Localで動作しているデータを参照したい場合は上記にかかれているように emulatorを実行後 localhost:4000 にアクセスすることで以下のようなUIでデータを確認することができます。
- 基本的にまっさらな状態からスタートするので、一度データを設定した後、2回目以降同じDBで始めたい場合は下記のように
export, emulator起動時にimport できます。
(base) firebase_emulator % npx firebase emulators:export exported2022MMDD
i Found running emulator hub for project at http://localhost:4400
i Creating export directory /XXX/firebase_emulator/exported2022MMDD
i Exporting data to: /XXX/firebase_emulator/exported2022MMDD
✔ Export complete
(base) firebase_emulator % npx firebase emulators:start --import=exported2022MMDD
Storybookでfirebase emulator を動かすための工夫
storybookのstoriesを追加する手順はシンプルです。テスト対象のPageを読み込んで表示するだけです。
import dashboard from '../../src/views/dashboard.vue';
import StoryRouter from 'storybook-vue-router'
// More on default export: https://storybook.js.org/docs/vue/writing-stories/introduction#default-export
export default {
title: 'Dashboard',
component: dashboard
};
/* story記述 */
// default
export const Default = () => ({ // 変数名がナビゲーションパネルでの表示名となる
components: { dashboard }, // 対象となるコンポーネントを指定する
template: '<dashboard />' // レンダリングするhtmlを記述する
});
Default.decorators = [
/* this is the basic setup with no params passed to the decorator */
StoryRouter({}, { initialEntry: { name: 'dashboard' } })
]
ただ、このままだと 当然firebaseの接続ができずにstorybookの表示でエラーが発生します。
そこで、下記のファイルにfirebase emulator への接続を追加します
// Initialize Firebase
import firebase from 'firebase'
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
console.log('preview initialized:',firebase)
const isEmulating = window.location.hostname === "localhost";
if (! isEmulating) {
console.error('error not localhost')
}
firebase.auth().useEmulator("http://localhost:9099");
firebase.firestore().useEmulator("localhost", 8090);
firebase.storage().useEmulator("localhost", 9199);
}
loaders
そして認証していることが前提のVueComponentがほとんどなので、
LocalのAuthにtest1 ユーザーが存在している前提で
Storybookを実行する前にLoginした状態をつくりたいです。そのためにstorybookに用意されているloadersの仕組みを利用します。
async function setupFirebase(){
const userCred = await firebase.auth().signInWithEmailAndPassword('test@test.com','XXXX')
console.log(userCred)
console.log(firebase)
return firebase
}
export const loaders = [
async () => ({
firebase: await(setupFirebase())
}),
];
firebase 参考、Tips
-
firebaseのプロジェクトを選択時に下記のようなエラーがでることがあります
% firebase projects:list ✖ Preparing the list of your Firebase projects Error: Failed to list Firebase projects. See firebase-debug.log for more info.
表示されているとおりfirebase-debug.log を開いてみると下記のError表示
Error: HTTP Error: 401, Request had invalid authentication credentials. Expected OAuth 2 access token
login できているって表示されてたのに、なぜ!!と思いながら上記のエラーメッセージでググると下記のパラメータで強制的に再認証シーケンスを走らせることで対応できるとのことです。
% firebase login --reauth --no-localhost
-
LocalでUIを立ち上げてっも白いページのまま表示されないことがあります。下記のコマンドでUIコンポーネントを再取得すると表示されるようになりました。
npx firebase setup:emulators:ui
-
Firebase 単体テスト公式情報わかりにくい
- https://firebase.google.com/docs/rules/unit-tests?hl=ja#rut-v2-common-methods
- 上記のサイトをみると いかにもemulatorを利用するにはinitializeTestApp, initializeTestEnvironment を呼び出さないと行けないように見えますが、実際には接続するだけであれば 追加のnpm install も必要なく 通常のfirebase libに上記で解説したとおり useEmulator を呼び出せばよいだけ。。 この情報にたどりつくまでに 大分時間かかりました。
storybookでpage 全体を動くようにするために しなければいけなかったもろもろものこと
-
storybook のwebpack build にて fs, tls, netがみつからないといわれる
- 下記を参考に.storybook/main.jsにwebpack configを定義しMockすることで回避
- StoryBookを開発プロジェクトへ導入した際にはまった点
-
asset の読み込みに失敗する
- 通常のvueのパスと異なるので imageの読みこみなどに失敗する
- https://storybook.js.org/docs/vue/configure/images-and-assets
- 上記を参考に main.jsに下記の設定を追加する
-
module.exports = { ... staticDirs: ['../../public',{from:'../../src/assets',to:'/assets'},{from:'../../src/components',to:'/components'}],
-
storybook内で vue $route を使うようにする設定がわからない
- 下記を参考にstorybook-vue-router を導入
- ただし、このプラグインは最近更新されておらず npm install時に 不整合が発生してしまう
-
npm install --legacy-peer-deps
と実行することで回避しているがvue3 にUpしてstorybook-vue3-routerに乗り換えたい
-
- https://qiita.com/sawami2019/items/85feb5c1603d6b535f00
-
storybook内でvuetifyを使いたい
- 下記を参考に.storybook/main.jsにwebpack css の設定を、 preview.jsにvuetify decoratorを追加
- https://qiita.com/wakana_t_miri/items/d1d13afbf3713346e8f0
-
storybookのvue component 内で
No Firebase App '[DEFAULT]' has been created - call Firebase App.initializeApp()
発生- 本体のpackage.jsonを誇大化させないように、storybook用のフォルダを分割し、storybookで必要な package.jsonはsub directoryで管理をしようとおもい、 storybookないのpackage.jsonでもfirebaseをinstallしていた。実態として.storybook/preview.jsで初期化したfirebaseと ../src/view/**.vueのなかで参照される firebaseが実態として違うもの(2重に)存在してしまっていたため。
- storybook/ 内のpackate.jsonのdependenciesからはfirebaseは削除。npm の仕様としてstorybookないのnode_modulesにfirebaseが存在しない場合は上位(../)のフォルダ内のnode_modulesを参照するので同じものが参照できるようになる
storybook実行
以下を実行し、表示できればOk
(base) storybook % npm run storybook
╭───────────────────────────────────────────────────╮
│ │
│ Storybook 6.4.19 for Vue started │
│ 43 s for manager and 36 s for preview │
│ │
│ Local: http://localhost:6006/ │
│ On your network: http://192.168.1.15:6006/ │
│ │
╰───────────────────────────────────────────────────╯
localhost:6006 にアクセスする
storycapの実行
以下を実行
(base) storybook % npm run screenshot
> storycap http://localhost:6006 --serverTimeout 600000
info Wait for connecting storybook server http://localhost:6006.
info Executable Chromium path: /XXX/node_modules/puppeteer/.local-chromium/mac-961656/chrome-mac/Chromium.app/Contents/MacOS/Chromium
info Storycap runs with managed mode
info Found 4 stories.
info Screenshot stored: __screenshots__/Home/Default.png in 3290 msec.
info Screenshot stored: __screenshots__/Dashboard/Default.png in 64422 msec.
info Screenshot was ended successfully in 68919 msec capturing 2 PNGs.
storybook/__screenshots__
以下にstorybookの画面ごとのキャプチャが格納される
github action 組込み
各自のLocalPCで実行するだけでなく、GithubでPullRequest作成時に自動でScreenShotを作成し、一つ前のバージョンとの差分を検出したい。それらを自動化してくれるツールがreg-suitです。
github actionで firebase emmulatorをinstall, storycapを実行し、それをGCPに保存、
reg-suitをかけた結果をGithub PRに表示してくれます。
name: VisualRegressionTest
on: pull_request
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
if: github.event_name == 'pull_request'
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
- uses: actions/checkout@v2
if: github.event_name == 'push'
with:
fetch-depth: 0
- name: Set up Node.js 16
uses: actions/setup-node@v2
with:
node-version: 16.x
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v0
with:
project_id: XXX
service_account_email: my-ci-account@XXX.iam.gserviceaccount.com
service_account_key: ${{ secrets.GCP_SA_KEY }}
export_default_credentials: true
- name: install japanese font
run: |
sudo apt install fonts-ipafont fonts-ipaexfont
- name: Run integration tests against emulator
working-directory: ./firebase_emulator
run: |
npm install -g firebase-tools
npx firebase emulators:start --import=exported2022MMDD &
- name: npm install main
run: |
npm install
- name: npm install for vrt
working-directory: ./storybook
run: |
npm install --legacy-peer-deps
- name: run storybook and screenshot(to avoid firestore initial load fail, do twice screenshot)
working-directory: ./storybook
run: |
npm run build-storybook
npm run screenshot-built
npm run screenshot-built
- name: run VRT
working-directory: ./storybook
run: |
npm run regression
env:
REG_NOTICE_CLIENT_ID: ${{ secrets.REG_NOTICE_CLIENT_ID }}
- name: upload artifact
uses: actions/upload-artifact@v2
with:
name: screencap
path: ./storybook/__screenshots__
PR上に下記のように通知がとどきます。めでたし、めでたし。
(今回の例だと新規のテストなので4つの画面が追加されたという白丸表示です)
github action 小ハマリポイント
- firebase emulator 実行方法
- 当初専用Containerを用意しないとだめかなと思っていましたが以下のコマンドで一発で通常のubuntu-latestにinstallできました
- name: Run integration tests against emulator
working-directory: ./firebase_emulator
run: |
npm install -g firebase-tools
npx firebase emulators:start --import=exported2022MMDD &
- reg-suit で detached_head といわれておこられてしまう。
- そもそもgithub actionのなかではgithubが独自につくったrefを参照しています
- 下記のようにcheckout時に pull_requestのheadを参照するように設定することで回避できます
- uses: actions/checkout@v2
if: github.event_name == 'pull_request'
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
- storybookの表示データがロードされていない
- どうもfirebase emulator 起動後一回目だけはどれだけまってからアクセスしてもloadに失敗する問題があるようです。詳しくは追えていないですが回避のためにかきで2度screenshotを実行しています
- name: run storybook and screenshot(to avoid firestore initial load fail, do twice screenshot)
working-directory: ./storybook
run: |
npm run build-storybook
npm run screenshot-built
npm run screenshot-built
以上です