【Firebase】Nuxt × Firebase でUnitテストを実施する
この記事では、FirebaseとNuxtを組み合わせたプロジェクトでのユニットテスト導入にJestを使用する際の簡単な手順を説明します。
2024年04月10日
関連記事
これまで通り、こちらのリポジトリを元に実装していこうと思います。
以下、今回の対象となるやつらです。
test
├── Executor.ts
├── firestore
│ ├── setupTest.ts
│ └── vegetable.spec.ts
└── libs
└── firebaseUtil.ts
coverage
└── firestore_rules
└── vegetable.html
jest.config.js
tsconfig.test.json
firestore.rules
前提条件
- Node.jsがインストールされていること。
- Nuxtプロジェクトがすでにセットアップされていること。
- Firebaseプロジェクトが設定されていること。
- Typescriptを使っている
細かいとこは省きますので、ハンズオン形式でトライする場合は差分の確認をおすすめします。
セットアップ
まずは必要なパッケージを入れていきます。
$ yarn add @types/jest jest ts-jest -D
Firebaseのセキュリティルールに沿ってテストを書いていくのでこちらも。
$ yarn add @firebase/rules-unit-testing -D
テストケースの確認
まずはセキュリティルールを確認しましょう。
- 書き込み権限は認証済みユーザーのみ許可。
- 読み取り権限は未認証でも許可。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isAuthenticated() {
return request.auth != null;
}
match /vegetables/{vegetableId} {
allow get: if true;
allow list: if true;
allow create: if isAuthenticated();
allow update: if isAuthenticated();
allow delete: if isAuthenticated();
}
}
}
readとwrite権限だけでも動作するのですが、せっかくテストするので詳細に書いてあげましょう。
セキュリティルールを読み込む
前提として、127.0.0.1:8080
でfirestoreが起動していることが必要です。
ルート直下にある firestore.rules
を読み込みます。
import {
initializeTestEnvironment as _initializeTestEnvironment,
RulesTestEnvironment,
} from '@firebase/rules-unit-testing'
import { readFileSync } from 'fs'
let testEnv: RulesTestEnvironment
export const initializeTestEnvironment = async (projectId: string) => {
testEnv = await _initializeTestEnvironment({
projectId,
firestore: {
host: '127.0.0.1',
port: 8080,
rules: readFileSync('firestore.rules', 'utf8'),
},
})
}
export const getTestEnv = () => testEnv
ルールに合わせて共通処理を
test/Executor.ts
にセキュリティルールでやることをまとめておき、各specで使い回します。
import { type KeyPair } from '@/types/KeyPair'
import { FirebaseFirestore } from '@firebase/firestore-types'
import {
QuerySnapshot,
deleteDoc,
doc,
getDoc,
getDocs,
setDoc,
updateDoc
} from 'firebase/firestore'
export default class Executor {
db: FirebaseFirestore
collectionName: string
constructor(
db: FirebaseFirestore,
collectionName: string
) {
this.db = db
this.collectionName = collectionName
}
collectionRef() {
return this.db.collection(this.collectionName)
}
documentRef(id: string) {
return doc(this.collectionRef(), id)
}
async get(id: string) {
return getDoc(this.documentRef(id))
}
async all(): Promise<QuerySnapshot> {
return getDocs(this.collectionRef())
}
async create(id: string, data: KeyPair): Promise<void> {
return setDoc(this.documentRef(id), data)
}
async update(id: string, data: KeyPair): Promise<void> {
return updateDoc(this.documentRef(id), data)
}
async delete(id: string): Promise<void> {
return deleteDoc(this.documentRef(id))
}
}
テストを書いていく
まず、セキュリティルールからどのようにテストを書いていくかを考えていきます。
権限は以下の通り。
認証済み | 未認証 | |
---|---|---|
get | ⚪︎ | ⚪︎ |
list | ⚪︎ | ⚪︎ |
create | ⚪︎ | × |
update | ⚪︎ | × |
delete | ⚪︎ | × |
認証済み、未認証をdescribeで括ります。
難しいことはしません。firebaseが用意してくれている assertSucceeds
, assertFails
関数で確認していきましょう。
import Executor from '@/test/Executor'
import { FirebaseFirestore } from '@firebase/firestore-types'
import {
assertSucceeds,
assertFails,
} from '@firebase/rules-unit-testing'
import { getTestEnv } from '@/test/libs/firebaseUtil'
import { setupTest } from '@/test/firestore/setupTest'
import { Vegetable } from '@/models/Vegetable'
import {
beforeEach,
describe,
it,
} from '@jest/globals'
setupTest('vegetable')
describe('Firestore Vegetable rules', () => {
let db: FirebaseFirestore
let executor: Executor
beforeEach(async () => {
await getTestEnv().withSecurityRulesDisabled(async context => {
const adminDB = context.firestore()
executor = new Executor(adminDB, Vegetable.MODEL_NAME)
const vegetables = [
{ id: 'vege_1_id', name: 'carrot' },
{ id: 'vege_2_id', name: 'pumpkin' },
{ id: 'vege_3_id', name: 'potato' },
]
for (const vege of vegetables) {
await executor.create(vege.id, { name: vege.name })
}
})
})
describe('認証済み', () => {
beforeEach(async () => {
db = getTestEnv().authenticatedContext('user_01').firestore()
executor = new Executor(db, Vegetable.MODEL_NAME)
})
it('取得:全てできること', async () => {
await assertSucceeds(executor.get('vege_1_id'))
await assertSucceeds(executor.get('vege_2_id'))
await assertSucceeds(executor.get('vege_3_id'))
})
it('一覧:全てできること', async () => {
await assertSucceeds(executor.all())
})
it('登録:全てできること', async () => {
await assertSucceeds(executor.create('new_id', { name: 'new_name' }))
})
it('更新:全てできること', async () => {
await assertSucceeds(executor.update('vege_1_id', { name: 'update_name' }))
await assertSucceeds(executor.update('vege_2_id', { name: 'update_name' }))
await assertSucceeds(executor.update('vege_3_id', { name: 'update_name' }))
})
it('削除:全てできること', async () => {
await assertSucceeds(executor.delete('vege_1_id'))
await assertSucceeds(executor.delete('vege_2_id'))
await assertSucceeds(executor.delete('vege_3_id'))
})
})
describe('未認証', () => {
beforeEach(async () => {
db = getTestEnv().unauthenticatedContext().firestore()
executor = new Executor(db, Vegetable.MODEL_NAME)
})
it('取得:全てできること', async () => {
await assertSucceeds(executor.get('vege_1_id'))
await assertSucceeds(executor.get('vege_2_id'))
await assertSucceeds(executor.get('vege_3_id'))
})
it('一覧:全てできること', async () => {
await assertSucceeds(executor.all())
})
it('登録:全てできないこと', async () => {
await assertFails(executor.create('new_id', { name: 'new_name' }))
})
it('更新:全てできないこと', async () => {
await assertFails(executor.update('vege_1_id', { name: 'update_name' }))
await assertFails(executor.update('vege_2_id', { name: 'update_name' }))
await assertFails(executor.update('vege_3_id', { name: 'update_name' }))
})
it('削除:全てできないここと', async () => {
await assertFails(executor.delete('vege_1_id'))
await assertFails(executor.delete('vege_2_id'))
await assertFails(executor.delete('vege_3_id'))
})
})
})
注意
ちょっとしたポイントをまとめておきます。
- 認証済みは
authenticatedContext
で設定。 - 未認証は
unauthenticatedContext
を使おう。 - setupTestはケバブケースで書こう。
実行方法
package.jsonに "test": "jest"
と設定してあるので、yarn test
で実行します。
以下のように出力されるはず。
PASS test/firestore/vegetable.spec.ts (5.831 s)
Firestore Vegetable rules
認証済み
✓ 取得:全てできること (554 ms)
✓ 一覧:全てできること (101 ms)
✓ 登録:全てできること (95 ms)
✓ 更新:全てできること (105 ms)
✓ 削除:全てできること (107 ms)
未認証
✓ 取得:全てできること (152 ms)
✓ 一覧:全てできること (100 ms)
✓ 登録:全てできないこと (84 ms)
✓ 更新:全てできないこと (142 ms)
✓ 削除:全てできないここと (87 ms)
Test Suites: 1 passed, 1 total
Tests: 10 passed, 10 total
Snapshots: 0 total
Time: 5.911 s, estimated 6 s
カバレッジは coverage/firestore_rules/xxx.html
のように出力されます。