OLTA TECH BLOG

テクノロジーと好奇心で事業を成長させる

TECH BLOG

INVOYフロントエンドにおけるテスト導入と方針

概要

OLTAのINVOYでフロントエンドエンジニアを担当している小林です.

INVOYは、請求書発行や共有に加え受取請求書の自動データ化やカード決済機能などを提供するサービスです.日々機能が追加される中で、ユーザーに安定したサービスを提供し続けるためには、不具合を防ぐためのテストが不可欠です.そこでINVOYのフロントエンドでは、E2Eテスト自動化ツールであるAutifyを導入し、テストを実施しています.これにより、大きな不具合もなくサービス提供が継続できています.しかし、サービスが成長するとともに、以下のような課題が顕在化してきました.

  • 品質への懸念:テスト不足による不具合リスク.E2Eのテストは実施コストが高く全てのパターンを網羅することはできていません.
  • 開発効率の低下:手動確認による時間的コスト
  • リファクタリングの阻害心理的ハードルが高く修正したくない → コピペ → 技術的負債につながる

これらの課題を解決するため、フロントエンドに対してテストコードを導入しました.

ちなみにINVOYは Nuxt/Vueを利用しています

対象読者

  • テスト導入を検討している人
  • テスト戦略を検討している人

方針

INVOYのフロントエンドにはテストコードがありませんでした.なので大前提としてテストコードを増やす必要性があります.

その大前提のもと以下の考えを戦略軸においています.

  • スモールスタート. 小さくテストをまばらにいれる
  • 網羅性は必要と考えない
  • mockは必要最低限にとどめ、ユーザの動作に近い形でテストする
  • コンポーネントのインタラクションやユーザの動作を中心にテスト

絞ったテストから始め、ユーザーの動作に近い形でのテストを重視しています. ユーザの動きが担保できることにより「テストで失敗しなければ変更しても良いんだ!」や「手動確認したけど大丈夫かな...テストあるから大丈夫やろ!」 と思えることが期待値です.

では、コンポーネントのテストは実際どんなテストを書くんだ?というのを記載します.

コンポーネントのテスト方針

テスト項目 有無 なぜ必要かあるいは不必要か
API実行のリクエストボディ
  • インタラクションに伴いロジックが動作するため
  • これが一番重要. 期待値が正しければシステムが正常に動作することを担保できる.よってクリティカルなエラーは防ぐことができる
formなどのインタラクション
  • 動くことでシステムが成り立つため
  • インタラクションに伴いロジックが動作するため
  • 入力したことでフォームが送信できることを確認、モーダルが開きボタンを押下することで次に進むなどをテストする
単体ロジック(composables, utils. etc) 🔺
  • コンポーネント結合テストの中でテストできるcomposablesに対してテストを書く必要はない
  • 細かなロジックはインタラクションではカバーできないものをテスト.あるいはcomposablesだけで完結するようなものはテスト書く
apiデータ取得のテストあるいはロジックに依存するビジュアテスト(v-if, v-show etc) 🔺
  • 網羅は必要ありません.間違うと信頼性を損なうようなデータ表示ならば必要.例えば権限の表示
  • あるいは分岐で大幅に画面が変更されるもの.
  • (願望) 本当はVRTなどのビジュアルリグレッションのテスト手法を用いたいのですが、そこまでまだやれてません.
globalmessageの表示やバリデーションエラーのテスト 🔺
  • 網羅は必要ありません.頻繁に触るコンポーネントであればあっても良いとしています.ですが、必須とはしていません.
  • try,catchで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では一緒に働く仲間を募集しています!!

corp.olta.co.jp

参考文献

以下はテスト戦略を考える際に参考にさせていただきました🙇