みんなで集合写真をとったのですが、何故か僕だけぶれてました。何歳になっても落ち着かないみたいです。
今日もfirebase大好きっ子が、firebase大好きっ子向けにfirebaseの情報を提供していこうと思います。
これはFirebase Advent Calendar 202123日目の記事です。
概要
本日はcloud functionsでunit testを書く際の方法として、Cloud Functionsのemulatorを使わないという選択肢とその利点と留意点について紹介します。
目標
cloud functionsのunit testを爆速にして荒ぶるCIを安定させます。
ただし、あくまで今回紹介するのは方法の一つです。
後述するデメリットはあるので必要に応じて取捨選択をしてください。
なぜCloud Functionsのemulatorを使わないのか
テストが安定しない。
この一言に付きます。
cloud functionsのエミュレータを組み合わせることで実際の挙動に近い形でfirestoreにデータを書き込んでCFが動いて・・・みたいなことをチェックできるようになります。
しかし、テスト結果が違ったり、タイムアウトしてしまったりとunit testが安定しないのです。
unit testが安定しない原因
根本的な原因として、テストの前後で、firestoreのデータの挿入や削除などを実施するのですが、それをトリガーとしてonDeleteやonCreateなどのCFが起動して複雑に絡み合ってしまい、データが変わってしまったりすることでテストが安定しなかったりします。
データを変わるのを待って・・・みたいな書き方もできるのですが、テストの実行順番によってデータが勝手に変えられたりみたいなことがよくありました。(涙)
unit testを安定化させる
タイトル回収です。
どうやって安定化させるかというと、おとなしくcloud functionsのエミュレータを使うのはやめましょう!!
では、どうやってunit testを書くかというところなのですが、cloud functionsに依存した書き方をやめるように変えるだけです。
つまり、CloudFunctionsの起動した後の処理に対してunit testを書くのです。
得られる効果
2点あります。
それは、安定化と爆速化です。
firestoreに書き込んだ際のCloudFunctionsのイベントのトリガーがなくなるので、純粋にテスト用データの準備やお掃除がしやすくなります。
わざわざCloudFunctionsの処理を待つといった処理をかかなくていいようになるのです!
つまり、テストも安定化しますし、CloudFunctionsの起動時間も削減できるので爆速化します!
どうやってテストを書くのか
例えば記事が作成されたらユーザの総記事数をインクリメントすると言った処理を作るとした場合、こんな感じでCloudFunctionsを書きます。
export default functions.firestore.document('articles/{articleId}')
.onCreate(async (snapshot, context) => {
try {
await new ArticleController(snapshot.ref, snapshot.data() as IArticle).onCreate()
} catch (error) {
// いい感じにエラー処理
}
})
export class ArticleController {
constructor(reference: FirebaseFirestore.DocumentReference, data: IArticle) {
// 必要な処理の準備
}
async onCreate() {
// 処理本体
}
}
このような形でCloudFunctionsで呼び出す処理はArticleControllerの処理だけで完結するようにしてあげます。
すると、テストする際にはArticleControllerをインポートすれば処理本体をテストすることができるようになります。
例えば記事を作成したらユーザの記事総数をインクリメントするといったテストの場合こんな感じになります。
import * as firebaseTest from "@firebase/rules-unit-testing";
const projectId = 'testproject'
const adminFirestore = firebaseTest.initializeAdminApp({projectId: projectId}).firestore()
describe('ArticleController create test', () => {
beforeAll(async () => {
await firebaseTest.clearFirestoreData({projectId: projectId})
})
beforeEach(async () => {
await adminFirestore.doc('users/mogmet').set({username: 'もぐめっと', articleCount: 0})
})
afterAll(async () => {
await firebaseTest.clearFirestoreData({projectId: projectId})
})
test('記事が作成されたらuser.articleCountがインクリメントされている', async () => {
await new ArticleController(adminFirestore.doc('articles/1'), {title: 'hoge'} as IArticle).onCreate()
const user = (await adminFirestore.doc('users/mogmet').get()).data()
expect(user!.articleCount).toBe(1)
})
})
@firebase/rules-unit-testingのライブラリを使ってテストするのが肝です。
security rule以外にもテストとして使えます。
応用事例
セキュリティールールを書く際にもっぱら使う@firebase/rules-unit-testingなのですが、cloud functionsのテストにも使えるので使いようによっては共通処理として書くこともできます。
もぐめっとはよく初期化処理や初期データ準備周りをまとめちゃってどちらのテストにも使えるようにしてます。
留意点
このやり方のデメリットとしては、cloud functionsが起動できているかどうかまでは担保することができません。
そのため、起動部分に関しては別途cloud functionsのemulatorを使ってunit testを書くなどの担保が必要になってきます。
ちなみに、もぐめっと的には処理の担保さえとれてればあとは実際にアプリを動かして確認すればいいと考えているので上記のテストは書いていないです。
まとめ
クラスもしくはメソッドに対してunit testを書くことでCloudFunctionsのunit testを安定させ、荒ぶるCI神を鎮めることができるようになりました。
更に爆速化することで開発速度も大幅にアップすることできます!
みなさんももしよかったら今回紹介したテストを使って爆速firebase開発ライフを送ってください!
最後に、ワンナイト人狼オンラインというゲームを作ってます!よかったら遊んでね!
他にもCameconやOffchaといったサービスも作ってるのでよかったら使ってね!
また、チームビルディングや技術顧問、Firebaseの設計やアドバイスといったお話も受け付けてますので御用の方は弊社までお問い合わせください。