概要
OLTAのINVOYでフロントエンドエンジニアを担当している小林です.
INVOYは、請求書発行や共有に加え受取請求書の自動データ化やカード決済機能などを提供するサービスです.日々機能が追加される中で、ユーザーに安定したサービスを提供し続けるためには、不具合を防ぐためのテストが不可欠です.そこでINVOYのフロントエンドでは、E2Eテスト自動化ツールであるAutifyを導入し、テストを実施しています.これにより、大きな不具合もなくサービス提供が継続できています.しかし、サービスが成長するとともに、以下のような課題が顕在化してきました.
- 品質への懸念:テスト不足による不具合リスク.E2Eのテストは実施コストが高く全てのパターンを網羅することはできていません.
- 開発効率の低下:手動確認による時間的コスト
- リファクタリングの阻害:心理的ハードルが高く修正したくない → コピペ → 技術的負債につながる
これらの課題を解決するため、フロントエンドに対してテストコードを導入しました.
ちなみにINVOYは Nuxt/Vueを利用しています
対象読者
- テスト導入を検討している人
- テスト戦略を検討している人
方針
INVOYのフロントエンドにはテストコードがありませんでした.なので大前提としてテストコードを増やす必要性があります.
その大前提のもと以下の考えを戦略軸においています.
絞ったテストから始め、ユーザーの動作に近い形でのテストを重視しています. ユーザの動きが担保できることにより「テストで失敗しなければ変更しても良いんだ!」や「手動確認したけど大丈夫かな...テストあるから大丈夫やろ!」 と思えることが期待値です.
では、コンポーネントのテストは実際どんなテストを書くんだ?というのを記載します.
コンポーネントのテスト方針
テスト項目 | 有無 | なぜ必要かあるいは不必要か |
---|---|---|
API実行のリクエストボディ | ⭕ |
|
formなどのインタラクション | ⭕ |
|
単体ロジック(composables, utils. etc) | 🔺 | |
apiデータ取得のテストあるいはロジックに依存するビジュアテスト(v-if, v-show etc) | 🔺 |
|
globalmessageの表示やバリデーションエラーのテスト | 🔺 |
|
ロジックに依存しないビジュアルテスト | ❌ |
|
このコンポーネントの方針からもわかる通り、細かいテストコードを書くことは重要視していません.薄く広く伸ばし、機能の流れをテストできれば問題ないと思っています.
テストコード設計
設計は2つです. domの取得の汎用化とAPI mockにおける設計です.どういう風に設計したかを共有します.
setup 関数を用いたdomの取得の汎用化
参考: Avoid Nesting when you're Testing
に記載のある setupの記述を参考にしています.簡単にいえば、コンポーネントに対しての重複コードを関数にまとめることです.説明はコードのコメントで記載しています.
function setup(options) { //レンダリングする関数 const wrapper = async () => { const render = await renderSuspended(Form, { props: { ...options?.props, }, }); return render; }; // フォームの情報を汎用的に取得できる const form = { amountInput: async (text:string)=> { const inputElement = await screen.findByTestId( 'input-data', ); await user.type(inputElement, text); } } // modalのフォームの情報を汎用的に取得できる(機能ごとにformのアクションは分ける) const modalFormAction = { input: async (text:string)=> { const inputElement = await screen.findByTestId( 'input-modal', ); await user.type(inputElement, text); } } return { wrapper, formAction, modalFormAction } } test('test', async () => { const {wrapper, formAction} = setup() //レンダリング const { emit } = await wrapper() await formAction.amountInput('hoge') })
API mockの設計
コンポーネントのレンダリング時に内部で fetch が実行される場合、APIが呼び出されます.テスト方針で示している通り、重視しているのは submit 時のリクエストパラメータです.
そのためここで設計の期待値は、リクエストパラメータが適切な値であるかを検証しやすいことです.
コードで表現すると以下になります. registerEndpoint
という関数は@nuxt/test-utils
のライブラリを利用したものです
- Testing · Get Started with Nuxt
// ./endpoints.ts (コンポーネントのテストファイルとエンドポイントの設定をするファイルを分ける) const mockEndpoins = { dataPostAPI: { api: () => // @nuxt/test-utils が提供してくれるエンドポイントの関数 registerEndpoint(`/api/data/`, { handler: async (event) => { const body = await readBody(event) // mockを関数として作成し発火させる endpoints.dataPostAPI.mock.request(body); return { 'status': "ok" }; }, method: 'POST', }), mock: { request: vi.fn(), }, }, } // component.test.ts // テストを実行するタイミングで `registerEndpoint`を発火させる mockEndpoints.dataPostAPI.api() test('test', async () => { const requestBody = {data: 'hoge'} /** * この間でフォームのインタラクション * フォームの中で submitを実行するとAPIが叩かれる */ // 呼ばれたmockの関数の引数が正しいかを検証する expect( mockEndpoins.dataPostAPI.mock.request, ).toHaveBeenCalledWith(requestBody); })
この2つの設計を軸に各種のコンポーネントにテストを導入しています
終わりに
今回はINVOYのフロントエンドのテスト導入の話と方針について記載しました.
まだまだ導入したてなので、テストが入っていないコンポーネントがたくさんあります.少しづつ導入してこれまでよりも保守性を高めていこうと思います.
OLTAでは一緒に働く仲間を募集しています!!
参考文献
以下はテスト戦略を考える際に参考にさせていただきました🙇