i Cubed Systems Engineering blog

株式会社アイキューブドシステムズの製品開発メンバーが、日頃のCLOMO開発の様子などを紹介します。

フロントエンドの単体テストで実感したメリット

アイキューブドシステムズでフロントエンドの開発を担当しているMiyazakiTakayukiです。本記事ではフロントエンドの単体テストで実感したメリットをご紹介します。

フロントエンドの単体テスト導入を検討している方の参考になれば幸いです。

フロントエンドの単体テストを導入した経緯

弊社の製品のCLOMO PANELは従来、フロントエンド・バックエンド共にRuby on Railsのみで構成されていました。現在、以下の構成へ置き換えるプロジェクトを進めており、CLOMO PANELの一部はすでに置き換わっています。

  • バックエンド
    • Ruby on Rails API
  • フロントエンド
    • Nuxt.js

Nuxt.jsでフロントエンドの開発を始めた当初はスナップショットテストのみで実装する方針でした。スナップショットテストとはレンダリングされたHTMLの変更を検知し、予期せずにUIが変更されていないかを確かめるテストです。しかし、実装した画面が増えてくるにつれ以下の課題が出てきました。

  • リファクタリングがやりにくい
  • 依存関係のあるライブラリ・フレームワークのバージョンアップの際のデグレを把握できない

リファクタリングはリファクタリング対象の挙動が変化しないように注意して行う必要があります。意図せず挙動を変化させてしまった場合、デグレが発生する可能性があります。単体テストが実装されていない場合は、このデグレを恐れてリファクタリングに着手するハードルが上がってしまいます。

ライブラリ・フレームワークのバージョンアップでは、使用しているAPIの廃止や仕様変更に対応する必要があります。この対応が漏れるとデグレが発生してしまいます。規模が大きくなるにつれ、ライブラリ・フレームワークのバージョンアップによるデグレを把握することは大変になります。

このような課題を解決するためにフロントエンドの単体テストを導入することとなりました。

どのようなテストを実装したのか

Nuxt.jsでの単体テストは、多くがコンポーネントのテストになりました。ここでは、どのようなテストを行ったかを簡単にご紹介します。Vue.jsに関する内容になるのでコンポーネントの基本|Vue.jsをご一読いただければ理解がしやすいと思います。

コンポーネントに対するテストの実装は、ガイド|Vue Test Utilsの記載に従い、入力(input)を与えたときの出力(output)が正しいかという観点でテストを実装していきました。入力には次のようなものがあります。

  • props
  • ユーザーの操作
  • 子コンポーネントのイベント

対して出力には次のようなものがあります。

  • 画面の変化(HTML CSS)
  • Eventの発行
  • 子コンポーネントに渡すprops

コンポーネントの単体テストはテストフレームワークのJestに加え、Vue Test UtilsというVue公式のテストライブラリを使用しました。具体的には以下のようなテストを実装しています。

削除ボタンを押下した場合、削除確認ダイアログが表示されているか

it('「削除」ボタンををクリックしたとき、削除確認ダイアログが表示されること', async () => {
  await wrapper.find('[data-testid="deleteButton"]').trigger('click')
  expect(wrapper.find('[data-testid="deleteConfirmDialog"]')).isVisible()).toBeTruthy()
})

バリデーションエラーが発生した場合、submitボタンにdisabled属性が付与されているか

it('文字数制限以上の文字を入力した場合、「保存」ボタンに disabled 属性が付与されていること', async () => {
  await wrapper.find('[data-testid="input-name"]')).setValue('a'.repeat(256))
  expect(wrapper.find('[data-testid="submitButton"]').attributes('disabled')).toBeTruthy()
})

inputの値を変更した場合、正しい引数でイベントを発行しているか

it('「名前」を変更したとき、イベントが発行されること', async () => {
  await wrapper.find('[data-testid="input-name"]').setValue('名前')
  expect(wrapper.emitted('update:name')).toEqual([['名前']])
})

子コンポーネントのpropsに正しい値を渡しているか

it('子コンポーネントの props に正しい値を渡していること', async () => {
  expect(wrapper.find('[data-testid="childComponent"]').props('name')).toBe('名前')
})

単体テストを導入してから約8か月間経過した現在ではテストケース数は5000件を超えてきました。

実感したメリット

単体テストにはメリットとデメリットが存在します。デメリットとしては単体テストを書く工数やテストコードのメンテナンスコストがなどがあげられると思います。まだまだ単体テストを運用しはじめてから約8か月と短い期間ですが、すでにデメリットを超えるメリットを実感しています。 実感したメリットの中から以下の3つを抜粋してご紹介します。

  • リファクタリングがやりやすくなる
  • 実装のミスに早く気づける
  • 共通コンポーネントの仕様変更が行いやすくなる

リファクタリングがやりやすくなる

1つ目は、リファクタリングがやりやすくなることです。これは単体テストを始めた理由の1つでもありました。単体テストがあるのとないのとではリファクタリングのやりやすさは雲泥の差だと思っています。

実際にリファクタリングを行った例をご紹介します。 フロントエンドの開発を進めて実装した画面数が増えてくると複数のコンポーネントでロジックの重複が起こるようになってきました。そこで、重複したロジックを再利用可能な共通関数として切り出すことになりました。

関数の切り出しを行った際、切り出し元コンポーネントの単体テストがしっかりと実装してあったため、手作業で動作を確認する必要はありませんでした。単体テストの実装を確認したのみです。今回の作業では共通関数化したものの数がそこそこ多かったので、単体テストがなかった場合は目視での確認になり、それなりに大変だったと思います。また、目視での確認は漏れが発生する可能性もあります。

単体テストを実装していれば確認作業に時間がかからず、確認漏れが発生することもありません。リファクタリングのやりやすさが格段に違います。

実装のミスに早く気づける

2つ目は、実装のミスに早く気づけることです。 私たちのプロジェクトでは新規に画面を実装した際、同時に単体テストを実装しています。この単体テスト実装時に考慮漏れなどのミスに気づくことがあります。

単体テストでは実装をできる限り細かい単位に区切ってテストを行っていきます。その細かい範囲でテストデータを準備していると考慮漏れに気づきやすくなります。また、意図せず他の箇所に影響を与えていた場合などにテストが失敗して気づくことができます。

単体テスト実装の段階で実装ミスや考慮漏れに気づけることはとても大きなメリットだと思っています。実装の段階で不具合に気づかずそのまま進めて社内検証やリリース後に実装ミスや考慮漏れが発覚すると不具合となり、不具合原因の調査や修正の対応への工数が発生してしまいます。単体テスト実装時にミスに気づけることで不具合対応工数の発生を未然に防げているという考え方もできると思います。

共通コンポーネントの仕様変更が行いやすくなる

3つ目は、共通コンポーネントの仕様変更が行いやすくなることです。 Nuxt.jsでは共通コンポーネントを組み合わせて画面を実装します。共通コンポーネントはさまざまな画面で利用されることとなります。使用される画面が増えれば増えるほど影響範囲が大きくなり共通コンポーネントを変更することはリスクが大きくなっていきます。しかし、さまざまな理由で仕様変更をする必要が出てくることがあります。

共通コンポーネントの仕様変更も単体テストを実装していることで行いやすくなりました。仕様変更によって共通コンポーネントの使い方が変わった場合に単体テストが失敗して影響範囲を教えてくれるためです。

Vue.jsやReactなどコンポーネントベースの開発を行うフレームワークでは単体テストによる恩恵を受けやすいと思いました。

キャッチアップとしての単体テスト実装

単体テストは上記のメリット以外にもキャッチアップとしてとても効果的だと感じました。

私は2021年に新卒として入社して現在2年目です。新卒としての研修(以前の記事で新卒の研修について紹介しています!)が終わった後、開発チームへの配属のために今後どのようなことをやっていきたいか希望を聞かれる機会がありました。研修中にフロントエンドの技術について興味を持っていたため、フロントエンドチームへの配属を希望し、希望通りに配属していただけることになりました。

私がフロントエンドチームへ配属されるタイミングとフロントエンドの単体テストを導入するタイミングがちょうど被っていたため、キャッチアップもかねて単体テスト未実装画面の実装を私が担当することとなりました。

今振り返るとこの単体テストの実装がキャッチアップとしてとても良かったと思っています。キャッチアップでは主にシステム全体像の把握や動いているシステムの確認、ソースコードの理解などを行っていくことが多いと思います。単体テストを実装することで、システムの確認、ソースコードの理解をより深く行えたと思います。

ソースコードの理解

単体テストはテスト対象の関数やコンポーネントの仕様を理解していないと実装できません。 何が渡されて何が返却されるかを理解する必要があります。そのため、実際に動かしてみたり、さまざまな入力パターンを試してみたりしました。単体テストを実装するために試行錯誤を繰り返していくうちにソースコードが理解できるようになりました。ただソースコードを読むだけの場合よりも、必然的に理解が深くなったと思います。

コンポーネントの分割基準の理解

コンポーネントをどのように分けていくかはチームやプロジェクトの方針で決まっている部分でもあるのでキャッチアップの段階で理解しておきたいところです。この理解にも単体テストが役に立ちました。

コンポーネントの単体テストでは対象のコンポーネントの責務を理解する必要があります。例えば、propsで値を受け取って表示する子コンポーネントがあり、その親コンポーネントを対象とします。対象のコンポーネントの責務は、子コンポーネントにpropsに値を渡すところまでです。 単体テストでは「子コンポーネントに渡しているpropsの値が正しいか」ということをテストします。 もし、「子コンポーネントの表示が正しいか」ということをテストしていた場合、子コンポーネントでpropsの値を表示せずに内部で扱うように修正すると、親コンポーネントの単体テストも修正する必要が出てきます。このように、単体テストでは責務を理解することが重要です。

私もコンポーネントの責務を意識しながら単体テストを実装しました。コンポーネントの責務を理解していくことでコンポーネントの役割がわかるようになり、どのような役割でコンポーネントが分割されているかが理解できました。

おわりに

フロントエンドの単体テストを導入し、どのようなテストを行い、どのようなメリットを実感したかをご紹介しました。私自身まだまだ単体テストについて理解が足りていない部分も多々あるのでさらに勉強して、単体テストでユーザーや開発者などこの製品に関わるすべての人が幸せになれることを目指して頑張ろうと思っています!

最後に、弊社では採用活動を実施しています。 皆様のご応募をお待ちしております。 www.i3-systems.com