🛠️

フロントエンドのテスト構成について考えてみた in 2023

2023/09/22に公開1

はじめに

この記事では、

  • フロントエンドの開発において意義のあるテストはなにか?
  • それらをコスパよく実現するためにはどうすればよいか?

について考えて、作った構成を紹介します。

前提

下記の技術スタックを利用していますが、これ以外のスタックでも応用可能な仕組みが多いと思います。

  • Next.js
  • Storybook
  • playwright
  • msw
  • msw-snapshot (拙作)

注意事項

この記事の構成は、まだまだ実験的な機能だったり怪しい技術が一部採用されています。

  • msw-snapshot
    • 拙作のライブラリであって、動作が怪しい可能性がめっちゃあります。
  • Next.js の testmode
    • playwright + msw を実現するために必要でした。
    • まだまだ全然まともに動かないかもしれません。(サンプルリポジトリの単純なテストは動いた)

サンプル

下記のリポジトリにサンプルを用意しています。

https://github.com/hrsh7th/frontend-testing-demo

このリポジトリの構成を利用すると、下記のようなテストが利用可能になります。

Unit / Smoke Test

目的

例えば、30 個のコンポーネントが存在する状態で radix-ui のバージョンを上げる みたいなことは、そこそこ発生する作業な気がします。
このようなケースでは、すべてのコンポーネントに対してランタイムエラーが発生していないか?を簡易的にでも確認できると安心できそうです。

方針

@storybook/test-runner を利用して、すべての UI コンポーネントを Smoke Test します。
複雑なコンポーネントは @storybook/addon-interaction を使って Unit Test をします。
サンプルの package.json#scripts.test:smoke あたりに実装してあります。

この仕組みは UI コンポーネントを Storybook で開発している状態さえ作れれば勝手に維持されます。
非常に高コスパで便利です。

Visual Regression Test

目的

例えば 30 個のコンポーネントが存在する状態で tailwind.config.js の設定を変更する といった場合、実際にどの UI コンポーネントに変化があるか?を人力で把握するのが困難です。
このような場合には、機械的に全 UI の表示差分を確認できる仕組みが必要になります。

方針

main ブランチが更新されたら、main ブランチのビジュアルスナップショットを作成して GitHub Actions の artifact に保存します。
あとは Pull Request 毎の CI で artifact をダウンロードして差分確認をするだけです。

こちらの仕組みも UI コンポーネントを Storybook で開発している状態さえ作れれば勝手に維持されます。

この仕組みは CI 周りが少しだけトリッキーですが、サンプルはかなり単純化してあるので見ればわかると思います。

Integration Test

目的

フロントエンドがバグってしまう原因は様々考えられます。

  • なんらかのパッケージを更新したところ、挙動が変わっていてバグる
  • UI コンポーネントをリファクタしたら、意図しないページがバグる
  • 普通にポカミスでバグる

こういったことを防ぐためにはリグレッションテストが欲しくなりますが、我々が開発しているのはウェブサイトであるため、可能であれば実際のブラウザを利用したシナリオベースのテストを書いていきたいところです。

方針

実際のブラウザを利用したシナリオベースのテストは、フロントエンドエンジニアの悲願だと思われますが、様々な課題があります。

  • API を叩くテストは不安定になりがちなのでモックをしないといけない
    • API のモックを用意するのはかなり面倒です。
    • モックは古くなります。メンテが必要でかなり面倒です。
  • 外部システムに依存するテストになりがちで、CI による自動化が困難になりがち
    • バックエンドシステムが新 API をリリースしない限り CI できないといったことが考えられます。

これらを軽視して仕組みを導入しても「書くのがだるく、自動化されていない」のでテストが腐っていきます。

これを解決するために、playwright + msw + msw-snapshot を用いて下記のようなワークフローを構築してみました。(まだ「やってみました」という段階で、実運用に耐えるかどうかはこれから検証します。)

  1. ローカル開発時は msw-snapshot を無効化する
  2. ローカル開発時に E2E テストを書きながら開発する
  3. 開発が完了したら msw-snapshot が生成した API スナップショットも一緒にコミットする
  4. CI 環境上では msw-snapshot を有効化することで全 API がモック込みでテストされる

msw-snapshot は拙作のライブラリですが、今のところそれっぽく動いています。
スナップショットの生成条件などはまだまだ調整が必要そうなので、これから頑張っていきます。

E2E Test

目的

例えば、バックエンドチームが API のリファクタリングをリリースしたところ、フロントエンドチーム管轄のウェブサイトが壊れてしまった!ということは普通にありえるケースかと思います。
このようなリグレッションは Integration Test を API モックなしで継続的に実行することができれば自動的に検出することができそうです。

方針

下記のようなタイミングで msw-snapshot を無効化した Integration Test を走らせることができれば目的は達成できそうな気がします。
(実際は flaky なテストが増えそうなので、やってみたら全然ワークしないかも...)

  • main ブランチが更新されたタイミング
  • バックエンドチームがリリースしたタイミング
    • ステージング環境を対象にすればリリース前の検知も現実的かもしれません

おわりに

現段階でのフロントエンドのテスト構成 in 2023 を紹介してみました。

正直、Integration Test や E2E Test はこれから実験していくいった段階であり、まだ上手くワークするかどうかすらわかっていませんし、拙作の msw-snapshot にも色々バグがあるかもしれません。

テスト戦略を考える上では「テストにはコストがかかる」ということを忘れず、バランスを取りながら意思決定することが大切だと思います。
闇雲に「テストは必須!!!」とするのではなく、目的を考えた上で「安くて美味いテスト」を求めて研究を続けていきます。

Discussion

hrsh7thhrsh7th

正直、Integration TestE2E Test って何が違うの?って思っている人間だったりしますが、この記事は Integration Test = モックを活用して決定性を高めた E2E Test という解釈で書いています。

お前の解釈はおかしい!みたいな指摘はめっちゃ歓迎なのでぜひコメントください!