フロントエンドのコンポーネント設計について自分なりのベストプラクティスを考えてみたのでアウトプットする。
業務でVue + TypeScriptを使っているのでVueを想定。
Reactでもuse-.tsのファイルをhooks、.vueを*.tsxと考えれば代替えできると思う。
メルカリさんのメルペイのフロントエンドのテスト戦略はとても参考になり、結構自分のテスト戦略、コンポーネント設計において影響を受けた。
※随時更新予定
まずは各テストについての自分なりに考えたテスト戦略
単体テスト
使用するフレームワークはJest
単体テストはコンポーネントに依存しない複雑なロジックのユーティリティ関数系をテストを中心にテストする。
コンポーネントの単体テストは1つのコンポーネントで成り立つ機能はほとんどなく、基本的に結合テストで踏襲できることが多く、効果は大きくないので実施しない。
なのでコンポーネント実装の際には単体テストを実施しやすいようにコンポーネント内にロジックをもたないようにし、
コンポーネント自体はインタラクションとレンダリングにフォーカスするようにする
Vue.jsの場合はCompotion APIでreactiveやrefなどでstateの値を生成できるのでstate管理も関数化して、コンポーネントは該当関数を呼び出して、stateを取得してレンダリング等を書き換えていくのがいいとおもう。
結合テスト
使用するフレームワーク、ツールはJest、StoryBook、Mock Server Worker
意味のある機能のコンポーネント単位で行う
ある程度、1つの機能として成り立っているコンポーネントのインタラクションとレンダリングパターンをテストする。
内部的に呼び出しているコンポーネントも合わせてテストする。
機能コンポーネントによっては単純にviewを分割するようなコンポーネントもあるのであくまでコンポーネントは分割しておいて、レンダリングは一緒にテストするみたいにできるといいとおもう。
props等はモックを使い、APIなどの外部システムはMock Server Worker(以下、MSW)を使い、リクエストをインターセプトしてMockデータを返却するようにする。
ページ単位で行う
使用するフレームワーク、ツールはJest、StoryBook、Mock Server Worker
機能と機能で構成されたページレベルののインタラクションとレンダリングをテストする。
ここではパターンよりも意味のある機能と機能を連結した場合のテストにフォーカスする。
なのでパターンテストなどは行わない。
E2Eテスト、シナリオテスト
使用するフレームワーク、ツールはPlaywright
これは実際にAPI通信を活用して、手動でテストする。一部、E2Eテストフレームワークが使えるなら使いたい。(E2Eはこわれやすいので柔軟性をもたせるためにあえて手動という選択肢もありと考えている)
すでに単体テスト、結合テストを行っているのでここではあくまでシナリオに沿って動作を保証できるかにのみフォーカスするテストをする。
そのため、当たり前だが単体テスト、結合テストに比べてテストケースは大幅に少なくなる。
というか、なるべくテストケースが少なくなるように単体テスト、結合テストを充実させる。
自分の考えたコンポーネント設計
上記のテスト戦略を元に自分なりにテストしやすく、実装時に迷わず実装できるようなコンポーネント設計を考えてみた。
結論
__tests__
└__setup__
└__mock__
└{データ名}
index.mock.ts。
└unit
└lib
└{関連データモデル名}
└{関連データモデル名}.spec.ts
└{関連データモデル名}
└{関連データモデル名}.spec.ts
└story
└ui
└{デザインパーツ名}
└{デザインパーツ名}.story.ts
└feature
└{機能名}
Main{機能名}.story.ts
View{機能名}.story.ts
└{機能名}
Main{機能名}.story.ts
View{機能名}.story.ts
└pages
└{ページ名}
index.story.ts
└{ページ名}
index.story.ts
feature
└{機能名}
└type
index.ts
└use-main-{機能名}.ts
└Main{機能名}.vue
└Main{機能名}.scss
└View{機能名}.vue
└View{機能名}.scss
└{機能名}
└type
index.ts
└use-main-{機能名}.ts
└Main{機能名}.vue
└Main{機能名}.scss
└View{機能名}.vue
└View{機能名}.scss
pages
└{ページ名}
└type
index.ts
index.vue
└{ページ名}
└type
index.ts
index.vue
lib
└{関連データモデル名}
└{関連データモデル名}.ts
ui
└{デザインパーツ名}
└type
index.ts
{任意の名称}{デザインパーツ名}.vue
└type
index.ts
{任意の名称}{デザインパーツ名}.vue
repository
└api-get.ts
└api-post.ts
└api-delete.ts
auth
└index.ts
type
└{データモデル名}
└index.ts
└{データモデル名}
└index.ts
各ディレクトリについて説明していく。
tests
テスト系のファイルをすべてまとめるディレクトリ
setup
テスト実行時前にsetupする処理。環境変数やブラウザ依存のオブジェクトなど。
mock
各テスト実行時に使用するmockデータ、使用するデータごとにディレクトリを分ける。
このmockは単体テスト、結合テストで共通で使用する。
unit
lib
単体テストをまとめるディレクトリ。対になるlibと同じディレクトリをもち、各libの*.tsと対になるように*.spec.tsがある。
1つのファイルに1つのテストファイルがある形になっている。
story(integration)
story.tsファイル郡。
feature
結合テストをまとめるディレクトリ。featureとpagesに分かれており、それぞれのコンポーネントと対になる形で*.story.tsファイルがある形になっている。
feature内部に分割コンポーネントがある場合はコンポーネントごとにstory.tsを作成する。
ui
デザインパーツごとのstory.tsファイル郡。
テスト目的というより、カタログ的な意義を持つ。
feature
機能ごとにフォルダで管理して、以下のファイルで基本的に構成するようにする。
feature間の呼び出しは許容する。別のfeatureから別のfeatureのviewを呼び出すのは原則NG。
└type
index.ts
└use-main-{機能名}.ts
└Main{機能名}.vue
└Main{機能名}.scss
type
feature単位で使用するTypeScriptの型。
use-main-{機能名}.ts
Main{機能名}.vueのコンポーネントのstate系のロジックを管理するファイル。
Compotion APIからreactive関数やrefなどを呼び出して定義してlibフォルダ内のユーティリティ関数を呼び出してコントロールするのが主な責務。
Main{機能名}.vue
機能のメインコンポーネントで、機能のインタラクションとレンダリングの責務を持つ。
基本的に同一ディレクトリのuse{機能名}.tsからstateを受け取ってレンダリングしたり、インタラクションに応じてuse{機能名}.tsまたはlibフォルダ内のユーティリティ関数を呼び出す。
Main{機能名}.scss
Main{機能名}.vueのスタイルをまとめているファイル
メインコンポーネントのviewを単純に分割するユースケース
View{機能名}.vue
メインコンポーネントのtemplateが長くなった場合にviewを単純に分割することもあるため、専用のコンポーネント。
メインコンポーネントと同一ディレクトリに作成する。
└View{機能名}.vue
└View{機能名}.scss
View{機能名}.scss
View{機能名}.vueのスタイルをまとめているファイル
pages
ページのroutingを主としたコンポーネントで、機能などは持たない。
featureを呼び出してレイアウトする責務をもつ。
type
pages単位で使用するTypeScriptの型。
lib
データモデル単位のユーティリティ関数をデータモデル単位ごとにディレクトリを分ける構成。
データモデルはユーザー情報、記録情報などの単位で、それらに対するロジックをまとめる。
ui
ロジックを持たず、依存関係のないデザインパーツ的コンポーネント。
デザインパーツ単位は「Button」「Label」「Checkbox」など。
type
デザインパーツ単位で使用するTypeScriptの型。
repository
APIとのCRUD系のやりとりのユーティリティ関数ファイル群。
auth
認証系のユーティリティ関数ファイル群。
type
{データモデル名}ごとにindex.tsを持ち、featureをまたいで使うようなデータモデルの各関連する型情報がまとめられている。
まとめ
テストのしやすいかつ、迷わず実装できるようなコンポーネント設計についてけっこう試行錯誤したが自分なりに納得するような設計ができたのはほんとうによかった。
まだまだ、改良の余地もあるだろうし、課題もあるだろうからどんどん改善していきたい。