🔥

CypressからFirebase emulatorのFirestoreが読み込めないときは(with Angular)

5 min read

株式会社 CauchyE エンジニアの @yutaro_elk です。フロントエンドが好きでやってます。
現在は Ionic + Angular + Firebase で NFT 関連のアプリ開発をしています。

CauchyE は技術スタックとして主に Angular + Firebase を使用していますが、Cypress と Firebase + Angular を組み合わせた時に詰まった点があったので残しておきたいと思います。

最近 firebase sdk のバージョンが v8 から v9 に上がったことで、api がガラッと変わっています。
こちらの記事はは主に v8 での記載になっていますが、「おまけ」部分で少し v9 についても言及しています。

TL;DR

  • Cypress 経由で Firebase emulator の Firestore にアクセスするとデータが取得できない
  • firebase.firestore.settingsexperimentalForceLongPolling: true にしてあげる必要がある
  • Angular で AngularFire を使っている場合、 上記の内容は NgModule で DI してあげる必要がある

問題

Firebase を使ったアプリケーションを Cypress でテストをする時、Firebase emulator を使用することでテスト環境をクリーンな状態で実行することができて便利です。

ただ、上記の組み合わせで Firestore のデータを Cypress 経由で読み込もうとすると、うまく読み込めません。

ちなみにこんなことになる理由として firebase の Issue にこのような説明がありました。

https://github.com/firebase/firebase-tools/issues/1975#issuecomment-586824095

根本的な問題は、サイプレスがすべてのネットワークトラフィックを傍受しているため、監視したり、場合によってはモックしたりできることだと思います。ただし、firestore で使用される Web チャネルプロトコルには、同じ http リクエストに対して複数の応答があります。サイプレスコードはこれを処理できず、最初の応答のみを転送し、残りを無視します。
(google 翻訳)

Cypress が通信をインターセプトしてよしなに色々できる機能が、この場合邪魔してしまっているようです。

解決策

解決策としては firebase を init するあたりで firebase.firestore.settings に オプションとして experimentalForceLongPolling: true を設定してあげます。

firebase.firestore.settings({ experimentalForceLongPolling: true });

ちなみに解決する理由としては Cypress の Issue にこのような説明があります。

https://github.com/cypress-io/cypress/issues/2374#issuecomment-587879412

Firestore の WebSDK のデフォルトの動作は、WebChannel のストリーミングモードを利用することです。クライアントは XHR のように見えますが、サーバーは応答を 60 秒間開いたままにし、その時間枠の間にサーバーが開始した応答をできるだけ多く送信します。

ExperimentalForLongPolling オプションは、サーバーが要求ごとに 1 つの応答のみを送信するように強制します。
(google 翻訳)

テスト環境だけで設定するようにしよう

experimental な設定ということもありますし、experimentalForceLongPolling: true は Cypress 環境でのみ設定するようにしましょう。

たとえば window に Cypress が生えているかどうかでチェックすることができます。

if (window.Cypress) {
  firebase.firestore().settings({ experimentalForceLongPolling: true });
}

おまけ:Angular (+AngularFire) の場合

ここまでは Github の Issue にも直接解決策が載っていたんですが、Angular の場合もうひと手間かかります。

firebase sdk のバージョンが v8 から v9 に上がったことで、AngularFire(v6 -> v7) の api もガラッと変わりました。(記事を書いて公開を待っていたらその間に一気に陳腐化...)

AngularFire v6 と v7 で今回の問題についての解決策が変わっているので、新旧2つ書きます。

AngularFire v6 (firebase sdk v8)の場合

Angular で Firestore を利用するとき、ライブラリとして AngularFire を使うことが多いんじゃないでしょうか? その場合上記の firebase.firestore.settings はライブラリにラップされ直接アクセスできません。

そのため Angular の DI を使って設定してあげる必要があります。

import { SETTINGS as FIRESTORE_SETTINGS } from '@angular/fire/firestore';

@NgModule({
  providers: [
    {
      provide: FIRESTORE_SETTINGS,
      useValue: { experimentalForceLongPolling: true },
    },
  ],
})
export class AppModule {}

AngularFire から SETTINGS という InjectionToken を FIRESTORE_SETTINGS として import し、providers で useValue を使い設定用のオブジェクト注入する形です。

Cypress 環境かどうかのチェックや、エミュレーターの設定を加えると以下のようになります。

// ...
import {
  USE_EMULATOR as USE_FIRESTORE_EMULATOR,
  SETTINGS as FIRESTORE_SETTINGS,
} from '@angular/fire/firestore';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule,
    AngularFireAuthModule,
    FormsModule,
  ],
  providers: [
    // useEmulatorがtrueの場合はFirebase emulatorを使用する
    ...(environment.useEmulator
      ? [
          { provide: USE_AUTH_EMULATOR, useValue: ['localhost', 9099] },
          { provide: USE_FIRESTORE_EMULATOR, useValue: ['localhost', 8080] },
        ]
      : []),
    // Cypress上でのfirestore.settingsをDIする
    ...((window as any).Cypress
      ? [
          {
            provide: FIRESTORE_SETTINGS,
            useValue: { experimentalForceLongPolling: true },
          },
        ]
      : []),
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

AngularFire v7 (firebase sdk v9)の場合

AngularFire v7 (firebase sdk v9) になると DI で設定にアクセスするのではなく、firebase のインスタンスに直接アクセスできるようになりました。

const initApp = () => initializeApp(environment.firebase);

@NgModule({
  imports: [
    provideFirebaseApp(initApp),
    provideFirestore(() => {
      const firebase = initApp();
      const firestore = initializeFirestore(firebase, {
        experimentalForceLongPolling: (window as any).Cypress ? true : false,
      });
      if (environment.useEmulator) {
        connectFirestoreEmulator(firestore, 'localhost', 8080);
      }
      return firestore;
    }),
  ],
})
export class AppModule {}

参考

https://stackoverflow.com/questions/59336720/cant-use-cypress-to-test-app-using-using-firestore-local-emulator

https://github.com/cypress-io/cypress/issues/2374

CauchyE は一緒に働いてくれる人を待ってます!

ブロックチェーンやデータサイエンスに興味のあるエンジニアを積極的に採用中です!
以下のページから応募お待ちしております。

https://cauchye.com/company/recruit

Discussion

ログインするとコメントできます