React + TypeScript + Cypress で始める E2E テスト

AUTHOR :   ギックス

この記事は GiXo アドベントカレンダー の 24 日目の記事です。
昨日は、 React で作る中規模 SPA のレイヤードアーキテクチャ でした。

MLOps Div. の濱田です。
本日は 12 月 24 日ですね。学校では期末で、冬休みが近いのではないでしょうか。学期末といえばテストということで、今回は Cypress を用いた E2E テストについてご紹介します。なお、テスト対象の Web アプリケーションは React.js + TypeScript で実装されており、バックエンドに Firebase を活用しています。

テストの種類

せっかくの機会ですので、E2E テストの話をする前に、テストの種類について整理していきます。E2E テストの中身について知りたい方は、こちらの章は飛ばしてかまいません。

一口に「Web アプリケーションのテスト」といっても、その内容は多岐に渡ります。

JSTQB ( Japan Software Testing Qualifications Board ) によれば、テストは下記のように区分されます。

  • テストレベル ( 開発工程 ) に基づく分類
    • コンポーネントテスト
    • 統合テスト
    • システムテスト
    • 受け入れテスト
  • テストタイプ ( 品質特性 ) に基づく分類
    • ホワイトボックステスト
    • 機能テスト
    • 非機能テスト
      • ユーザービリティテスト
      • 負荷テスト
      • セキュリティテスト … etc.

そして、すべてのテストタイプは、すべてのテストレベルで実行できるとされています。

では、E2E テストはどこに分類されるのでしょうか。システムテストの一環として行われそうですが、E2E テストはワークフローを確認するという点で、適切でないとも考えられます。

…難しいですね。Stack Overflow にも、この手のテストの分類方法についての質問が数多く挙げられています。どのようなテストが存在するのか認識しておくことはもちろん大切ですが、さらに重要なのは個々のテストがプロジェクト内で何を意味し何を確認するためのテストなのか、認識を合わせておくことでしょう。

今回のプロジェクトでは、Web アプリケーションのリファクタリングを行う際に、E2E テストを導入しました。テストを実装する時間は限られていたため、「ユーザーがアプリケーション上で目的を達成できること」を最低限の目標としました。そのためここでは E2E テストを次のように定義していきます。
「実際にブラウザを操作し、ユーザーが Web アプリケーション上で目的を達成するまでの一連のシナリオを確認するためのテスト」

E2E テストの選択肢

E2E テストを行う上で、ライブラリの選択肢はいくつかあります。

  • Cypress
  • Puppeteer
  • TestCafe
  • Selenium

この中でも導入のしやすさ、実行時間の早さ、GitHub のスターの数、インストール数 ( ≒ ドキュメントの豊富さ ) など諸々を考慮して Cypress を選択しました。実際に使ってみると、シンプルに実装できながらも、高機能で使い勝手は良いです。さらについ先日、4,000 万ドルの資金調達を発表したとのことで、今後のさらなるサービス拡大が期待されます。他の選択肢として、 Autify は有償ながらもノーコードでテストシナリオが作成できるということなので、いずれ触ってみたいところです。

Cypress を導入する

React の新規プロジェクトを作成するところから始めます。

必要なライブラリをインストールします。

ディレクトリ構成

下記の構成を目指します。

テストファイルは root > e2e 配下に配置し、src とは別のディレクトリで管理します ( TypeScript Deep Dive でも推奨されています ) 。理由としては、意図せぬ lint エラーを避けるためです。例えば、Cypress は Mocha の構文を利用しているため、多くのコマンドが Jest と重複します ( describe beforeEach expect など ) 。ディレクトリを分離することで、 lint 時に意図せず Jest の警告が発生することを防ぎます。

root ディレクトリにおける設定

package.json に下記を追記します。

  • Windows 環境で実行できるように、cross-env を指定しています
  • cypress コマンド実行時に NODE_PATH=src を指定することで、テストディレクトリから src 配下のモジュールを絶対パスでインポートできるようになります。

yarn start でアプリを立ち上げてから yarn cy:open を実行すると Cypress の UI が立ち上がり、サンプルのテストファイルが表示されます。

また、root ディレクトリ配下に cypress ディレクトリが作成されます。私は、こちらのディレクトリ名を e2e に変更して運用しています。

ディレクトリ名を変更したので、cypress.json に下記を追記します。

ここまでで、ひとまず Cypress によるテストを実行可能な環境が整いました。

テストコードを TypeScript で動かせるように設定する

テストコードの拡張子は .js になっていますが .ts として扱えるようにします。
e2e ディレクトリに移動します。integration/examples ディレクトリは削除し、package.json を作成します。

必要なライブラリをインストールします。

.eslintrc.js を作成することで、lint が効くようになります。

これで .ts ファイルを扱えるようになりました。e2e 配下の .js ファイルの拡張子をすべて .ts に変更してください。そのうえで 1 つ、新しくテストを作成してみます。 e2e/integration/sample.spec.ts を作成し、下記を記述してください。

下記のように code タグ内に src/App.tsx という文字列が存在することを確認するテストです。

yarn cy:run を実行すると、無事にテストが通ることを確認できるはずです。

スナップショットテスト

UI が変わらないことを確かめる際は、スナップショットテストを導入すると良いでしょう。e2e 配下で、必要なライブラリをインストールします。

e2e/plugin/index.ts を下記のように変更します。

e2e/support/commands.ts を下記のように変更します。

root ディレクトリの package.json でスナップショットテストを行うスクリプトを追加します。また、既存のスクリプトでは failOnSnapshotDiff=false として、スナップショットテストを行わないようにします。

前章で作成した e2e/integration/sample.spec.ts に、スナップショットテストを追加しましょう。

yarn cy:snap を実行すると、snapshots/sample.spec.ts/test.snap.png が作成されます。こちらがスナップショットです。

この状態で一部の文字を red に変更して、再度テストを実行してみます。
テストが失敗し、snapshots/sample.spec.ts/__diff_output__ に差分のスクリーンショットが保存されていることが確認できます。

左が元の画面、右が変更後の画面、中央が差分の画面です。

ここまでで、無事にスナップショットテストが実行できることを確認しました。

テストカバレッジを取得する

root ディレクトリにて、ライブラリを追加します。

package.json に追記します。

e2e/plugins/intex.ts を下記のように変更します。

e2e/support/index.ts を下記のように変更します。

yarn cy:run を実行すると、コンソールにカバレッジが表示されるようになります。

GitHub Actions で CI を行う

package.json に、Chrome と Firefox で Cypress による CI を実行するためのスクリプトを追加します。

.github ディレクトリを作成し、workflow を定義した yml ファイルを作成します。

e2e-test.yml に下記を記述します。
コンテナイメージは、こちらから選択可能です。

これで、 push 時に E2E テストが実行されるようになりました。

その他 Tips

ダッシュボード

Cypress はダッシュボードを提供しています。

  • 月 500 テストまで、無料でテスト実行結果を保存しておけます
  • CI と連携させることができます
  • テストの動画を見ることができます
  • 結果をチームメンバーに共有可能です

有償プランで、制限を拡張できます。

環境変数の隠蔽

隠蔽したい下記のような情報は、cypress.json ではなく cypress.env.json で管理し、GitHub の管理から外すと良いです。

  • ログインテストを行う際のメールアドレス・パスワード
  • Firebase の接続情報
  • Cypress ダッシュボードの Key

これらを CI で利用したい場合は、GitHub Secrets にも登録しておきます。

Cypress のバージョン

Cypress は頻繁にバージョンアップされます。改善されるのは良いですが、最新バージョンはバグを含んでいる場合があります。バージョンアップは GitHub の Issue と照らし合わせつつ、注意して行ってください

テストを書き始める前に

テストシナリオをあらかじめ書き出して、そちらに沿って書いていくと良いでしょう。E2E テストはユニットテストに比べて時間がかかるため、必要な分だけ責務を持たせることが賢明です。

おわりに

Cypress を利用した E2E テストを紹介しました。時間がかかる、壊れやすいなどの弱点もありますが、目的に合わせて使えば有用です。実際、E2E テストのおかげで、安心感を持ってリファクタを進めることができました。興味を持たれた方は、ぜひ導入を検討してみてください。

現在、弊社のフロントエンドは主に React + TypeScript + Firebase で開発を進めています。こちらの記事をご覧になっている方々は近い環境で開発を行っていると推察されますので、下記のアドベントカレンダーもオススメです。

最後に宣伝を…
私が所属する MLOps Div. では絶賛メンバー募集中です。紹介記事はこちら。興味を持っていただけた方は、是非目を通してみてください。
さらに、私達と一緒に働きたい!と思っていただけた方がいらっしゃれば、ぜひこちらからご応募ください。まずはカジュアル面談で話だけ聞いてみたい、という方も歓迎です。

さて、いよいよ明日は 大トリ、弊社代表の網野より「データに基づく判断・意思決定の環境を整え、世界の考える総量を最大化するために」を公開予定です。お楽しみに!


Shota Hamada (濱田 翔大)
MLOps Div. 所属
Kaggle 、将棋、コーヒー。React + TypeScript によるフロントエンドの開発を担当しています。

SERVICE