【書き起こし】Frontend Testing: Cypress as a Testing Platform – Wilson Lau【Merpay Tech Fest 2021】

Merpay Tech Fest 2021は、事業との関わりから技術への興味を深め、プロダクトやサービスを支えるエンジニアリングを知れるお祭りで、2021年7月26日(月)からの5日間、開催しました。セッションでは、事業を支える組織・技術・課題などへの試行錯誤やアプローチを紹介していきました。

この記事は、「プラットフォームとして、Cypresssを利用したフロントエンドのテスト」の書き起こしです。

Wilson Lau氏:「Merpay Tech Fest 2021」へ、ようこそ。Wilsonと申します。プラットフォームとして、Cypresssを利用したフロントエンドのテストについてご紹介します。

私は、2020年にメルカリ・メルペイに入社して以降、当社のフロントエンド開発に携わっています。社内の加盟店管理ツール全般、加盟店ダッシュボード、加盟店申し込みフォームなどです。メルペイとしての大きな取り組みの1つが、テストの推進です。今回は、今までの成果についてご紹介します。

背景から少し説明させてください。メルペイは、2019年2月ごろに提供をスタートし、それ以降、非常に多くのものを構築してきました。当社にとって、常に迅速に新機能を開発していくうえで、フロントエンドを含むテスト全般というのは非常に重要です。

これまでテストを重視してきた主な理由の1つは、開発速度を維持するということです。こうした製品開発で自信も維持したいのです。テストを実行する大きなメリットの1つとして、QAのマニュアルテスト要件を減らせること、そしてQAチームと連携することでリリースの安全性も確保しています。膨大なテストを自動化テストにすることにより、時間を大きく節約することができます。

2つ目に、 タスク管理やリファクタリングの際にコードを壊すことなくリリースや変更することがとても重要です。テストの実行もそうです。3つ目に、この2年でメルカリが急成長したことでチームも大きくなり、テスト実行は新人メンバーにとっても重要です。

今日皆さんに学んでいただきたいのは、当社のテストは仕様書の代わりであるということです。テストを読み込んだり、実行したりすることで、その製品がどんなものか、特定の手段による多様なシナリオも自分以外の人に理解してもらえます。これは、新人メンバーにとって非常に有効なことです。最後に、新機能の開発でもスピードアップにつながります。

ほかにも、当社のツールキットにあるものは、テストプラットフォームの一環で開発した完全モックのバックエンドです。開発する際にバックエンドのモックが可能で、バックエンドの開発と同時に新機能の作成ができます。さらに私たちは、テストコードを機能コードと同時に書けるようにしました。開発速度がかなりアップすることが想像できると思います。バックエンドにも、テスト設計にも依存しないため、すべてを同時にできるので開発速度が非常に速くなります。

今日は、主要なテストスタックにおけるCypressの使い方を紹介します。Cypress以外のツールも使っていますが、ユニットテストの一例です。Cypressを主要プラットフォームに使うことで、テストが抜群に強力になりました。同じスタックで複数のテストが可能で、構築した共有オブジェクトを使用できるからです。例えばAPIモックや共有ユーティリティレポートシステムなど、テストで共有するものすべてです。これでテスト設計が楽になります。では、その最も基礎から始めます。プラットフォームはもちろん、Cypressです。

Cypressとは何か? Cypressは、とてもすばらしいツールです。フロントエンドのテストにおいても、かなり画期的なものだと思います。Cypressは、Chromiumベースのブラウザをラップします。この画像にもあるようなテストラッパーです。このテストラッパーでクライアント側の事象にアクセスすることができます。例えばご覧のように、クライアント側のクエリが実行できて、クライアント側のすべてを見ることもできます。そして、POSTリクエストや一般のリクエストサーバーからのレスポンスを遮断することができます。Cypressは、こうしたテスト環境を提供し、その環境下のすべてをコントロールできるのが強みです。後ほど、私たちが構築したモックやほかのツールのところで、使い方についてもう少し詳しく話します。

では、なぜ、テストライブラリの中でもCypressなのか。私たちは、このすばらしいCypressの導入に至るまでにほかのテストライブラリも数多く試しました。ご存じのJestやJSDOMに加えて、Vue、React Testing Libraryも試しました。テストにおいて、Cypressがほかと大きく違う点は3つあります。私たちが発見した主要な点の1つが、概してモッキングが少なく、実際のブラウザ環境だったということです。JestやJSDOMを使ったことがある人なら分かると思いますが、本当のブラウザ環境を完全には再現することはできないので、多くのモッキング作業が必要となり、その作業の多くがかなり大変です。モッキングの量が多いとカスタムコードの作成も多くなりますが、その点、Cypressは基本的にブラウザ内でアプリケーションをスムーズに動かすことができます。モックがたくさんあると大変なうえ、テストの確実性に欠けてしまいます。問題がモックにあるのか、アプリケーションにあるのかが不明のため、非同期処理までモッキングしたり、保存したりするのは真の統合テストとはいえず、本来のアプリケーション処理とは異なります。

次に、JestとJSDOMをベースにしたテストが速いということです。DOM全体のレンダリングが不要なので、当然ですが、非同期処理で明らかな問題が起こります。その大きな問題の1つが、非同期の事象をテストしてしまう点にあります。テストライブラリでのミッションの1つは、明らかに非同期が発生するのかを待つことです。それはとても面倒なことであり、テストすることにいらだちを感じます。Cypressの利点の2つ目は、すぐにビジュアルフィードバックを得られることです。問題点が的確に分かり、テストのデバッグをすることができます。また、Cypressでは、基本的にテスト駆動開発が可能なことも強みです。進行しながらテストを書いていくのです。コード開発時に常に開いているブラウザといえます。ビジュアルフィードバックにはほかのメリットもあり、実際のDOMがあれば、それをテストすれば、コード確認や実装テストは不要です。「実装ではなく、動作をテストする」ということは、われわれのテストにおける教訓の1つです。ですから、関数が有効であっても、あるクラスが適用されても気にしません。ユーザー目線で、DOMに見えるものをもとにテストを実行します。これで、テストが堅牢になることが分かりました。

では、Cypressを土台にしているところから始めましょう。Cypress自体、強力ですが、ほかのアイテムと組み合わせるとさらに利便性が高くなります。各アプリケーション用に構築したものがフルセットのAPIモックです。アプリケーション内で使うエンドポイント用です。

それが、こんな感じです。すべてTypeScriptなので、型安全性が高いです。メルペイのバックエンドではgRPCを使っているため、Protocol Buffersを使ってさまざまな型が自動的に生成されてバックエンドに提供されます。すべてのモッキングを含めて型安全性がとても高いです。また、非常にシンプルな関数です。

すべてリクエストジェネレーターのモックであり、同じ関数を使って変更できます。また、CypressがAPIをモッキングするとリクエストパラメータが利用可能になるため、必要に応じてそれを使用できます。私たちはあまり使っていませんが使用可能です。こうしたモックがあるとどんな利点があるかといえば、テストしたい環境をいかようにも設定できます。時々、実際のテスト環境でさえもテストしたいシナリオの再現が難しい場合があります。フルセットのAPIモックがあれば、どんなことでもテストできます。

2つ目に、私たちはこのモックを頻繁に再利用しています。例えば、開発をしながらバックグラウンドで走らせるモックサーバーを実装できます。これをMock Service Workerと、Node.jsベースのカスタムサーバーに実装しました。モックの再利用可能性も非常に高く、大抵が満足のいくものでした。次に、共有ユーティリティが非常に重要だと分かりました。

これは一例ですが、Cypressでは「カスタムコマンド」をつくることができるテストスタックの中にそれをたくさん構築します。これはとても便利で、テストも読みやすくなります。

これは、私たちがつくったカスタムコマンドの一部です。例えばcy.mockというモックは、エンドポイントをモッキングします。また、Cypressが提供するユーティリティとAPIの上にカスタムのアサーションも構築しました。共有ユーティリティを構築する重要な理由の1つは、テストの読み書きがしやすくなることで、私たちが書いたカスタムユーティリティのようなものがあれば、テストが読みやすく、アサートのポイントが明確に分かります。

次に、私たちはCypressの「関数チェーン」も使って、さらにテストを読みやすくしました。

例えばここにあるようなcy.getByTestIdや、すべてのチェーンに対するアサーションなどです。Prettierを使うことでそのセクションをさらに読みやすくできます。

最後に、ファイルストラクチャやセクションの多用はテストを整理するうえで とても重要です。また、テストを書きやすくするため、これらの関数は多数の反復コードを避けてまとめています。

また、同じような関数を使うことでテスト全体で一貫性のある同じテスト構造を保つことができます。決まった方法で関数を書くことで見えるDOM要素のみ、テストさせるようにしたのです。

最後に、Cypress上に構築したレポートです。これが私たちがつくったものです。

Cypressのコードカバレッジを使って通常のカバレッジを取り、カスタムでcy.task関数やcy.log関数を設けてカスタムレポートのようなものもつくりました。こうしたテストを1日1回実行します。カスタム設定したCronジョブや、CircleCIを使います。GCPとCloud Loggingを使って全テストレポートをアップロードすると、Lookerで全データを表示するダッシュボードを生成できます。

これは、私たちのレポートの一例で、統合テストすべてのコードカバレッジです。さらに詳細が分かるレポートもあります。

ダッシュボードのレポートで重要なのは、評価された結果を管理することです。これは すべてのアプリケーションにわたるテストを進めるためにやるべきことの1つです。そして、それが非常に効果的だと分かりました。

とても視覚的で、テストした箇所が明確だからです。

E2E Regression Testing

続いて、Cypress上に構築できるさまざまなテストについて見ていきます。まず、E2Eリグレッションテストですが、Cypressで最もよく使われます。

私たちは、QAチームと合同で行いました。モックを全く使わず、同じ共有ユーティリティを使用しました。これがテスト環境での実施内容です。すべてのリリース前のQA期間にテストを実行しました。これは非常に役立ちました。どのアプリケーションも壊れていないことが確認できましたし、QAチームの時間節約にもなりました。このために私たちが構築したものが、PostmanのJSON形式を使ったカスタムヘルパーです。Postmanは、QAチームも使用するユーティリティです。彼らはPostmanにテストリクエストを保持しているため、私たちはJSONファイルを動かすカスタムコマンドを使い、Cypressで修正なく構築することができました。これがCypressにできる強みなのです。すべてのリグレッションテストで、テスト環境でテストしたいデータ作成に使えます。

Integration Testing

統合テストというものは少々大変です。

チームによって、それぞれ定義が違うと思われるからです。私が思うに統合テストで最も重要なのは、全フロントエンドコードの統合、つまり、フルクライアントです。それに含まれるのは、コード内のビジネスロジックから状態管理ライブラリのストアや、Vuex構築したコンポーネントなどのすべてです。われわれの経験から、モッキングはAPIのみで他は不要です。それがCypress内の完全に機能的なアプリケーションです。ですから、環境設定とテスト実行で私たちがやることはAPIレスポンスをモックし、DOMにユーザーアクションを起こします。アサートやアウトプットしたものがDOMに正確にレンダリングされ、ユーザーがアクションを起こすとき、正確なAPIリクエストを作成できます。先ほどの話と同じように、極力テストするのは動作であり、実装ではないということです。統合テストが包括的に行うテストの種類は、大きくこの4項目です。まず「レンダリング」です。レスポンスで正確な内容をレンダリングするか「パーミッション」はレスポンスと権利状態に基づいてユーザーにその許可を与えているか、表示は正しいかなどを判断します。3つ目は「バリデーション」で、ユーザーがフォームを送信または入力する際に、その入力が正しいかを検証します。「アクション」は、ユーザーによるボタンのクリックやフォーム送信の際、正確なリクエストを生成してDOMを正確に更新しているかです。

これは、説明したテスト構造の一例です。大きな見出しにレンダリングバリデーションアクションとあり、各セクションに特化したテストを書いています。これはテストの整理にとても役立ち、希望のテストケースをすべて網羅できました。

繰り返しますが、これはとても強力です。なぜならば、仕様明記の観点から内容をテストできるからです。モックではなく、DOMでの実際の内容なのです。共有ユーティリティとAPIモックにより、この統合テスト環境内でどんなテストでも可能です。

Accessibility Testing

次は、アクセシビリティテストです。Cypressの大きな強みとなるもう1つの点は、エコシステムが優秀なことです。皆さんは、多くのプラグインやツールを構築してCypressのコア機能をさらに拡張させたり、アクセシビリティテストを導入すると思います。

ですが、私たちは主要な統合プラグインにCypress-axeを使用します。その場合、 チーム全体でテストする標準アクセシビリティにもaxeを採用しています。必要なページのモックや設定がすでに完了しているため、これは非常に簡単にできました。基本的に、全ページのレンダリングに同じモックとユーティリティを使い、テスト用のCypress環境を設定するダイアログやアクションをオープンにしました。レンダリングしたページにaxeでテストして結果を記録します。例えばページ毎にアクセシビリティ妨害の数を測定します。また、合格に必要なページ数の基準値を設けて、すべてを記録してGCP内に格納しました。アクセシビリティテストがとても簡単にできるのが強みです。すべてが同じCypressコマンドなので、統合に特化して構築したすべてのものを各プロジェクトで共有できます。

Performance Testing

次もまた似ていますが、パフォーマンステストです。

私たちは、Lighthouseを使います。パフォーマンスには代表的なツールです。先ほどと非常に似ていますよね? 同じAPIモックを使っているし、共有ユーティリティもダッシュボードも同じものです。これらの環境設定ができたらページをレンダリングして、そのパフォーマンステストを行います。Cypressのパフォーマンステストで特筆するなら、さまざまな環境で実行できることです。シングルページアプリケーションか、サーバー側でレンダリングされたものか、どちらを構築するか選べます。APIモックか、テスト環境におけるAPIのどちらかを選べるのです。そのため、より詳しくパフォーマンスを診断できます。SPAやSSRなどは、クライアント側の問題なのか、レスポンスタイムの問題なのか、バリエーションを多くつくれることは大きな強みです。現在のパフォーマンスのあらゆる問題をより細かく診断できるからです。Lighthouseが提供するものも同じようなパフォーマンス指標で、ページ単位で評価されます。同じダッシュボードシステムを使うため、常にあらゆる指標を追跡できます。

Visual Regression Testing (Future)

では、まだ実践できていないけれども、今後やってみたいことは何か。それは、Cypressで簡単に実行できるビジュアルリグレッションテストです。

このテストは、パフォーマンスとアクセシビリティに似たようなパターンになると思います。どんなバージョンのページでもレンダリングできるので、ページの履歴から旧バージョンのページと比較して変更点について検証可能です。Cypressなら簡単にできると思っています。考えられるオプションはたくさんあります。Cypress-plugin-snapshotsや、Cypress-visual-regression、Cypress-image-diffなど、多くのツールが可能です。それもCypressのエコシステムが優秀だからです。これができれば、新たな大きな一歩だと思います。現在の取り組みであり、目標の1つは、テスト結果を表示する独自インターフェースの構築です。その構築作業によって、このテストタイプの構築は滞っています。しかし、近い将来、独自の仕組みの構築が成功するという100%の自信があります。リリースの視覚的検査を行うQAチームの負荷も大幅に減らせるでしょう。QAチームに旧バージョンと現バージョンの違いが分かるインターフェースがあれば、変更がなかったか、正確な修正がされているか、検証するだけでいいのでとても楽になります。

最後に、Cypressのエコシステムで新しいものがコンポーネントテストです。

これについては私たちも試みていますが、現在はまだStorybookとJestの組み合わせで実行しています。Cypressのコンポーネントテストは4月に発表されたかなり新しいものです。その違いはページ全体ではなく、各コンポーネントをレンダリングしてCypressで使えるすべてのAPIを使うことが可能な点です。DOM処理のヘルパーも使えますし、同じアサーションも使用可能であり、たくさんの使用コードが再利用できます。ですから、高いポテンシャルを持っています。Cypressはグローバル変数のモッキングを簡単にするため、たくさんのサポートを構築しています。問題点といえば、まだかなり新しくStorybookとJestのほうが安定性があるということです。ですが、Cypressの強みは双方向性にあり、コンポーネントの型によってはとても重要です。当社でも将来は、StorybookとJestの代わりとなるかもしれません。まだ、Cypressの可能性と今できることを示したにすぎず、まだまだ育てていきたい新しく大きな機能です。

再度、このスタックを見てみましょう。テストスタック全体で考えられる重要な点は、コードの再利用可能性に尽きます。Cypressを基盤プラットフォームにすべてのテストを実行すれば、APIモック、共有ユーティリティ、同じレポートシステムが得られ、テストの構築が極めて簡単にできます。そして、私が考えるのは、当社のチームでテストを推進する者として、場所を問わず率直にテストを推進する者として、テストを簡潔かつ快適にすることは非常に重要なことだということです。私がテストを書き始めた当初は、ほとんどの人がフロントエンドエンジニアでした。テストにあまりなじみがない人です。多くのフロントエンドチームはテストを書きません。それにフロントエンドがテストを書くのは難しいのです。典型的なユニットテストや統合テストは、大体バックエンドの仕事だからです。そのほかはフロントエンドの仕事ですが、テストは未知の領域です。そして、昨年あたりに私たちは、非常に安定した方法でテストを書く術をはっきりと明らかにすることができました。非常に直感的な書き方だと思います。フロントエンドはクライアントとの双方向に精通しており、Cypressなら自然な手段でそれを行えてテストも書けます。テストの簡略化をもっと促進できれば、Cypressはテスト設計で最も重要な基盤となり、将来はさらに多くのテストを構築できます。そして、自分のアプリケーションにテストが自信をもたらすことをさらに保証することとなり、格段にスムーズかつ迅速に開発できます。そういうことなのです。フロントエンドでのより良いテスト方法について理解いただけたらうれしいです。特に、比較的安定するコンポーネントテストや、単体テストの先にあるようなテストもCypressではとても簡単にできます。

ご清聴いただきありがとうございました。「Merpay Tech Fest 2021」へのご参加に感謝いたします。以上で終わります。