@firebase/testingをやめて@firebase/rules-unit-testingを使おう

2021/10/04追記： rules-unit-testingのバージョンがアップして使い方が変わりました。[紹介記事](https://zenn.dev/moga/articles/firebase-rules-unit-testing-v2)を書いているのでよければこちらもご覧ください。
https://zenn.dev/moga/articles/firebase-rules-unit-testing-v2

---追記終わり

2020年8月20日に[Firebase SDKの7.19.0がリリース](https://firebase.google.com/support/release-notes/js#7.19.0)されました。このバージョンではTestingSDKに以下のアップデートが入りました。

![スクリーンショット 2020-08-23 9.49.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74853/70a131bf-a634-1243-5442-9708bf3a5718.png)

`@firebase/testing`のいわば後継として、新たに`@firebase/rules-unit-testing`がリリースされました。名前が変更されて、何のためのパッケージなのかわかりやすくなりました。対応PRは、引用RTでmonoさんが教えてくださった[こちら](https://github.com/firebase/firebase-js-sdk/pull/3378)になります。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://t.co/SmfbOhJNTf">https://t.co/SmfbOhJNTf</a> が対応プルリクで、そこにこの変更に至った議論のリンクが載ってたけど中の人しかアクセスできなかった(　´･‿･｀) <a href="https://t.co/KrKn1FrBJR">https://t.co/KrKn1FrBJR</a></p>&mdash; mono (@_mono) <a href="https://twitter.com/_mono/status/1296991753082269696?ref_src=twsrc%5Etfw">August 22, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

軽く目を通してみると、このリリースに至った経緯はGooglerしか見ることができませんでした。しかし、破壊的変更が発生するにも関わらずこのリリースを行ったことを考えると、今後はこの新しいTestingSDKを使っていくほうがよいでしょう。

## 違い
ReleaseNoteに記載されているとおりですが、以下の違いがあります。

- インターフェースは互換性がある
- `assertFails()`の挙動が変わる
    - いままではassertFailsの引数に渡したPromiseがrejectされればfailとしていたようだが、今回からFirestoreのrulesに弾かれたときに発生する`PERMISSION_DENIED`のときのみfailとするようになる。これにより、より正確なrulesのテストになる。
- `initializeAdminApp()`が`firebase-admin`依存になる
    - 今までは`firebase-admin`を模倣したインスタンスを返していたが、新しいパッケージでは`firebase-admin`を使ってインスタンスを返すようになる

## 対応

`@firebase/rules-unit-testing`をインストールします。すでに`@firebase/testing`を使っている場合は、不要になるのでアンインストールしておきましょう。

```zsh
$ npm uninstall @firebase/testing
$ npm i -D @firebase/rules-unit-testing
```

import文を書き換えます。

```diff
- import * as firebase from '@firebase/testing'
+ import * as firebase from '@firebase/rules-unit-testing'
```

書き換えが完了したら一度テストを実行してみるとよいでしょう。テストの書き方によってはいくつかFAILしているかもしれません。それは、`set`メソッドで`Timestamp`や`FieldValue`が`invalid object`とか`invalid data`というエラーの場合は下記が原因です。

## client用Firestoreとadmin用Firestoreは別物

下記のようにする必要があります。

```TypeScript
import * as firebase from '@firebase/rules-unit-testing'
import * as admin from 'firebase-admin'

const projectId = 'sample'
const databaseName = 'test'

const clientFirestore = firebase.initializeTestApp({ projectId, databaseName }).firestore()
const adminFirestore  = firebase.initializeAdminApp({ projectId, databaseName }).firestore()
const clientUserRef = clientFirestore.collection('users').doc()
const adminUserRef  = adminFirestore.collection('users').doc()

// OK: Client用のFirestoreには@firebase/rules-unit-testingから生成したFieldValueを渡さなければならない
clientUserRef.set({ createdAt: firebase.firestore.FieldValue.serverTimestamp() })

// NG: Client用のFirestoreにfirebase-adminから生成したFieldValueを渡すとエラーになる
clientUserRef.set({ createdAt: admin.firestore.FieldValue.serverTimestamp() })

// OK: Admin用のFirestoreにはfirebase-adminから生成したFieldValueを渡さなければならない
adminUserRef.set({ createdAt: admin.firestore.FieldValue.serverTimestamp() })

// NG: Admin用のFirestoreに@firebase/rules-unit-testingから生成したFieldValueを渡すとエラーになる
adminUserRef.set({ createdAt: firebase.firestore.FieldValue.serverTimestamp() })
```

ようするにFirestoreに渡す値はパッケージが一致している必要があるということです。これが一致していないとエラーになります。実際、私が担当しているプロダクトのTestingSDKを差し替えるとこれに起因するエラーがいくつか発生していました。（いままでは、逆に、admin用のFirestoreにfirebase-adminから生成したTimestampを渡すとエラーになっていたと思う）

## おわりに
何か間違っている点などありましたらコメントいただけると幸いです。また、Firebaseに関する発信を[Twitter](https://twitter.com/_mogaming)と[Youtube](https://www.youtube.com/channel/UCmbwk54FJxio-rBN2o6L7zQ)でおこなっているので、よければ見てください。

## 追記
この記事を書いたことがきっかけで、実際に`@firebase/rules-unit-testing`を試してくれた人がいて、そのおかげで`firebase-js-sdk`にコントリビューターになれました！SDKで怪しい挙動があったらGithubに行ってコードを読んでみるといいかもしれません。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">firebase-js-sdkにPR送ってたんだけどマージされた🎉 🎉はじめて大きいOSSにPR送ったけどめちゃくちゃうれしい〜〜〜普段愛用しているOSSにContributeできるとこんなにうれしいんだな…<a href="https://t.co/yw6ekCH2Z9">https://t.co/yw6ekCH2Z9</a> <a href="https://t.co/jI4W79Iyd3">pic.twitter.com/jI4W79Iyd3</a></p>&mdash; moga🍳 (@_mogaming) <a href="https://twitter.com/_mogaming/status/1298431280195891201?ref_src=twsrc%5Etfw">August 26, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>