すっかり春っぽい陽気になったりならなかったりですが、いかがお過ごしでしょうか。なんで旅行にいくときだけ嵐のパターンが多いのか。

さて、相変わらずTypeScriptとばっかり格闘しているのですが、そのなかで若干の知見というかなんというか・・・を得たので、簡単に記事にしてみます

なにをしたいのか

まぁタイトルにあるとおり、playwrightを使ってEnd 2 End テストを書きたいんです。要件としては↓みたいな感じ、と自分で勝手に決めてます。

前まで触ってたJIRAのツリー表示アプリでは、firebaseを純粋なhostingとして使っていたのと、APIのmockとしてmswを使ったりしていたので、2・3番目が成立できない感じでした。今は別アプリの大規模リファクタリングを敢行しているんですが、そっちはauthやrealtime databaseを利用しているため、どうしてもemulatorとの接続が必要になります。

playwrightを採用したのは、お仕事でも使ってるからでもあるんですが、今回のアプリは、 複数のブラウザ を同時に扱わなければならないため、frameベースであるcypressでは多分実現が超むずい感じになるのがすぐ見えたためです。Cypressがwebdriverベースだったらできたかもしれませんが。

playwrightで複数のページを扱う

まずこれができないと、今回のアプリのE2Eができません。これについては、公式ドキュメントにもヒントがあったりしますが、 fixture を使うことで実現できます。

↓みたいなfixtureを作ると、きちんと終了時に掃除までしてくれます。


export const test = base.extend<{ newPageOnNewContext: Page }>({
  newPageOnNewContext: async ({ browser }, use) => {
    const context = await browser.newContext();
    const page = await context.newPage();

    await use(page);

    await page.close();
    await context.close();
  },
})

browser.newContext は、簡単に言えば新しいincognito/privateなBrowserを用意できるやつです。これをやらないと、authが共有されてウギャーってことになるので。

firebaseのデータ初期化

さて、firebaseなんですが、私はできるだけデータを流用したくない(並列に実行する、ということを目指す以上、前後に依存を持たせるのはアンチパターン)のです。

UT/ITだと、 @firebase/rules-unit-testingというライブラリがあって、こいつが都度都度異なるdatabaseへの接続をサポートしてくれたりして、他のテストで被らないようにできる・・・みたいな素敵なことができます。

が!!

実際にemulatorでauthを使う場合、これは当然ながら使えません。CIのときだけtestingEnvironmentを作ることは避けたいのもそうなんですが、 authと壊滅的に相性が悪いです 。なぜかというと、firebaseのauthはproject単位で存在しているのですが、emulatorはproject毎にしか立ち上げることができないから、です。(私の理解上)

つまり、authで作成したuidをdatabaseに保存して〜とかをする場合は、そもそも別のnamespaceを利用したりはできませんし、sign upを各々のtest caseで実施することもできません。本来は並列で実行したかったんですが、この制約と、nsを変更することの難易度から、 現状並列実行は諦めてます

起動時のfirebaseへのインポート

emulatorは、起動時に --import <directory> を指定することで、exportしたものを取り込んで初期化することができます。これをやると、前回の状態を再現したり、それなりに作った初期データを配布したり、ということも可能になります。

が、これもEnd2Endではとても使いづらいことがわかりました・・・。

なんでかというと、 起動時 にしか指定できないので、起動した後には 再インポートすることはできない のです。もしやりたければ、都度emulatorの起動からやり直す必要がありますが、かなりヘルシーではないやりかたになる上、結局直列なのは変わりません。

REST APIでの初期化

さて、ここでREST APIの登場です。firebase自体のREST APIも利用できるんですが、 一部emulator専用のAPIが存在します 。主にauthですが。

https://firebase.google.com/docs/reference/rest/auth?hl=ja#section-auth-emulator-clearaccounts

↑のAPIを実行することで、authに追加された全てのアカウントを初期化することが出来ます。これによって、何度でも同じメアドでのアカウント登録が可能になります。しかし当然ですが、都度uidが変わるため、realtime databaseなどに登録されたデータも初期化する必要があります・・・。

realtime databaseについては、emulator用のAPIなどはないのですが、PUTするのに加えて、bodyとしてお望みのJSONを渡すことで、全体を初期化することが可能になります。

https://firebase.google.com/docs/reference/rest/database?hl=ja#section-put

これを合わせて、次のようなfixtureを作成しました。

export const test = base.extend<{ resetFirebase: () => void }>({
  resetFirebase: async ({ request }, use) => {
    await use(() => {});

    const firebaserc = JSON.parse(fs.readFileSync("./.firebaserc"));

    await request.delete(`http://localhost:9099/emulator/v1/projects/${firebaserc.projects.default}/accounts`, {
      headers: { authorization: "Bearer owner" },
    });

    await request.put("http://localhost:9000/.json?ns=local-default-rtdb", {
      data: JSON.parse(fs.readFileSync("./misc/ci/database_export/local-default-rtdb.json")),
    });
  },
})

試した感じでは、emulatorに対してはauthorizationとか渡さなくても動きました。authの方は無いと動かないのですが。なお注意点としては、 projectはfirebasercに書いてあるもの を指定する必要がある、ということです。最初ベタ書きにしてまずいことに後で気がつきました・・・。 realtime databaseの方は、nsを指定することで、対象のnsを初期化できます。このときのnsは、productionコードで渡しているprojectId + default-rtdb となる模様です。私は local で用意しているのでそのまま使ってますが。

playwrightの方が色々できてなんとかなる

とりあえず二つのfixtureを組み合わせて、複数のブラウザからログインし、realtime databaseで更新されていることを確認し・・・みたいなことができるようになりました。たぶんcypressだと仕組み上出来ない感じがするので、こういったこともできるplaywright推しが強くなった感がします(まぁplaywrightの方が大分新しいのでアレなんですが)。

惜しむらくは、firebaseを利用したEnd2Endでは、事実上並列実行が不可能である・・・という点です。が、これについてはある程度諦めやすい(ケースを絞ったり、一つのシナリオを長くしたり)ので、まぁこれはこれでいいかな、といったところです。結構色々調べて時間を使ってしまったので、誰かの参考になれば幸いです。