はじめに
こんにちは。PharmaX株式会社エンジニアの古家です。
この記事はPharmaXアドベントカレンダー2022の16日目の記事になります。
15日目は川端の「深くネストされたObjectをDeep Copyする方法」でした。
今回はPharmaXの新規事業のフロントエンド開発環境にテストを導入した際に検討したことについて書いていきたいと思います。
テストを導入する際に行ったこと
以下の手順でテストの導入を進めていきました。順番に説明していきます。
- テスト導入前の開発チームの状況整理
- テストを書く目的の明確化
- 一般的なフロントのテスト手法の調査
- 自社としてどのテストを書くかの方針決め
- サンプルコードの実装
テスト導入前の開発チームの状況整理
サーバーサイドはRails、フロントエンドではNext.js+TypeScriptを利用しているチームで、事業フルコミットのフロントエンド専任のエンジニアは0名でした。
他の事業と兼任していたり、バックエンドやマネージャーの人がフロントエンドを書いたりするようなチームです。バックエンドでの経験はあるものの、フロントエンドでのテスト開発経験はほぼ皆無のような状況でした。
テストを書く目的の明確化
弊社では以下2点についてテストを書く目的と定めています。
生産性高く開発をし続けるため
テストが書かれていないレガシーコードが与える影響としては、次のような項目が挙げられます。
- 機能が増えるごとに手動テストのコストを肥大化させる
- 変更が不具合を与える影響を検知できないので変更コストが上がる
結果としてリリースを経るたびに開発の生産性が下がっていくことになります。
バグやリグレッション※による会社への損失を防ぐため
バグが起きるとサービス自体あるいはサービスの重要な機能が停止し、サービスのユーザーおよびサービスの運営会社に損失を与えることになります。
停止したら致命的な機能からテストを厚く書くことにより、会社の信用を損なうリスクを減らすことができます。
※ リグレッション:プログラムの変更に伴い、他の箇所でバグが発生すること
一般的なフロントのテスト手法の調査
react-testing-libraryの作者である Kent C. DoddsのブログにあるTesting Trophyの分類が分かりやすいため、参考にさせていただきました。
E2Eテスト
実際の動作による一連のフローが想定通りの結果となるかをテストするためのもの。フロントエンドだけではなくバックエンドも総合したテストを行うことができます。
ブラウザ上で動作し、実際の動作による一連のフローを全てカバーするため、信頼性は高いものの、実装コストが最も高く画面の仕様変更による修正も大変です。またテストの実行時間も長いです。
主にcypressやSeleniumを使用します。
インテグレーションテスト
コンポーネントやhooksなどの複数の部品を組み合わせて正しく動作するかをテストするもの。E2Eとは逆にアプリケーションの振る舞い部分のみテストするためAPIはモックします。
Next.jsでいう所の pages
コンポーネントに相当するコンポーネントのテストです。
テスト観点としてはレンダリング・アクション・バリデーションが挙げられます。
- レンダリング
- 画面の初期表示
- ステータスによる表示出し分け
- APIのレスポンスに応じた表示出し分け
- APIエラー時のメッセージ表示
- アクション
- ユーザーの操作に応じた画面の変化
- フォーム送信時のAPIリクエストの結果
- バリデーション
- フォームのバリデーション結果
仕様書をベースに対象ページの振る舞いが期待通りかをテストします。複数ページに跨るフローをテストしたい場合はE2Eを使いましょう。実行速度や実装コストはユニットテストとE2Eテストの中間に位置しています。
主にreact-testing-libraryやcypressを使用します。
ユニットテスト
関数単体のロジックのテストやコンポーネント単体のコンポーネントテストがあります。
E2Eテストやインテグレーションテストと比較して、最も結果のフィードバックが早く、実装コストも低いです。ただしプログラムの単体レベルでしか動作を保証できません。
コンポーネントテストはインテグレーションテストでカバーできるので、ユニットテストはhooksに書かれた共通のロジックをテストするくらいが良さそうです。
主にjestを使用します。
静的テスト
eslint 等を使った静的解析や、TypeScript 等を使った静的型チェック、typoチェックなど。
現代のフロントエンドにおいては、ベースラインとして必須となります。
ビジュアルリグレッションテスト
Testing Trophyの分類には該当していませんが、こちらは実際にブラウザに描画されたときの見た目のデグレを防ぐためのテストです。
変更前後のスクリーンショットを比較することでデザイン通りの見た目が維持されているかを自動で確認することが可能です。通常だと手動でチェックするしかない所なため、導入できると非常に有用なテストでしょう。
主にstorybook+storycap+reg-suitを使用します。
自社としてどのテストを書くかの方針決め
弊社のフロントエンド環境はbulletproof-reactを参考にしており、featuresディレクトリ以下に機能毎にサブディレクトリを切る構成になっています。
最初の方で書きましたが、弊社の開発チームには事業フルコミットのフロントエンドエンジニアがいなかったため、コストパフォーマンスの高いテストを選ぶことを重視しました。
featuresディレクトリの動作さえ担保することができれば、一通りの機能は抑えられるはずと考え、まずはトレードオフの観点でバランスのよいインテグレーションテストを厚めに書くことにしました。
方針
- features/機能名/components/index.tsx のインテグレーションテストを書く
- 上記のコンポーネントが機能単位のContainer Componentの役割
- このコンポーネントを抑えておけば、すべての機能の挙動をカバーできる。
- ツールはreact-testing-libraryを使う。
サンプルコードの実装
方針は決めたものの具体的にどんなコードを書けばよいかのサンプルが無いとチームメンバーが書き始められないため、jest+react-testing-libraryを使ってサンプルコードを実装しました。
describe('TestPage', () => {
test('ボタンのラベルがOKと表示される', () => {
render(<Test />);
expect(screen.getByTestId('ok')).toHaveTextContent('OK');
});
test('ボタンのラベルがNGと表示される', () => {
render(<Test />);
expect(screen.getByTestId('ng')).toHaveTextContent('NG');
});
});
実際やってみて
インテグレーションテストをどこまで書くか?テスト観点は何か?の認識を合わせるのが非常に難しかったです。この記事を書いている時点では知見が溜まっているため、上手く整理することができているのですが、導入当初では何となくの理解しかできていませんでした。
また、フロントエンドのテストは仕様が明確化されていないと厳しいなと感じました。今回テストを導入したPJは新規事業のため、仕様もふわっとしており、作りながらアップデートしていくという形式でした。そういう状態ではインテグレーションテストで、どの振る舞いを担保すべきなのか?と認識を合わせることが難しいです。もっとカッチリとInputとOutputの仕様を固めて誰がみても相違がないくらいにしないといけないですね。
最近ではcypressを使ったE2Eテストを書き始めたのですが、こちらは重要なユーザーストーリーのフローを抑えることが目的です。そのため書き始める前にどのユーザーストーリーを抑えたいのか、どんなフローになるのかのテスト設計を綿密に行うことが重要だなと感じています。
最後に
バックエンドはInputとOutputの仕様が同一のエンジニアの中で完結することが多いので、一人でテストを書いていきやすいのですが、フロントエンドはシステム利用者が相手なので綿密な事前のテスト設計が欠かせません。よりユーザーファーストなプロダクト開発を目指して、PharmaXフロントエンドのテスト環境を今後も進化させていきたいと思います。