はじめに

こんにちは、株式会社 MG-DX で Web フロントエンドエンジニアをしている柳萬真伸です。

私は 2024 年の 4 月に新卒で入社し、主に薬急便というサービスのフロントエンド開発を担当しています。

フロントエンドチームでは 2024 年秋頃より、フロントエンドのテスト環境を強化しています。

この記事では、私たちがテスト環境を改善するために行っている取り組みについてご紹介します。

薬急便について

MG-DX では、医療や薬局における DX の支援を目的として「薬急便(やっきゅうびん)」というサービスを提供しています。薬急便とは、薬局でのお薬の受け取りをオンラインで予約・受付できる「処方せん事前送信」の機能や、オンライン服薬指導などの機能を提供するサービスです。

薬急便では、ユーザー画面、薬局管理画面、クリニック管理画面、自社向け管理画面など、複数の画面を Monorepo 構成で管理しています。また、薬局のチェーンごとに UI や機能をカスタマイズしているため、複雑な仕様の箇所も多く存在します。

既存のテスト環境は、開発初期段階においてサービスを迅速に構築するために必要なテストが実装されていました。具体的には、スナップショットテスト、ビジュアルリグレッションテスト、最低限のユニットテストとインテグレーションテストが含まれていました。

サービスが成長し拡大する中で、安定性を確保しつつ生産性を維持するためには、テストの強化が必要だと考えました。

テストガイドラインについて

テストを強化するにあたり、チーム全員が共通の認識を持てるようにテストガイドラインを作成しました。このガイドラインは、基本的なテストの書き方や考え方を定義し、開発チーム全体で統一したテストアプローチを持つことを目的としています。これにより、テスト品質を高く保ち、テスト観点の抜け漏れを減らすことができると考えています。

テストガイドラインには、以下の項目を書きました。

  1. テストガイドラインの目的
  2. テスト戦略
  3. Storybook について
  4. 単体テスト・結合テストについて

ドキュメントのように全てを網羅することよりも、ガイドラインとしての柔軟性と読みやすさを重視しました。

テストガイドラインの内容を簡単に説明します。

テスト戦略

テストガイドラインを作成するにあたり、テスト戦略を改めて考えてみました。

テストを入れていく際に重要なことは、どの観点のテストを優先的に入れていくかを決めることだと考えています。

フロントエンドのテストには、Unit テスト、Integration テスト、E2E(エンドツーエンド)テスト、ビジュアルリグレッションテストなど様々な種類があります。しかし、全てのテストを満遍なく入れようとすると保守が大変であったり、工数的に難しい面が出てきます。

自分たちのプロダクトや開発体制に適したテストを重点的に書いていく必要があり、私たちはテスティング・トロフィー型を目指していくことにしました。

テスティング・トロフィー型を表した図

具体的には、ユーザー操作や API 通信を含む中間レベルのコンポーネントや処理に重点的にテストを行います。一方、低レベルの UI コンポーネントや高レベルの E2E(エンドツーエンド)テストは、必要最低限のテストに留めます。

このアプローチにより、少ない工数でテストの効果を最大化できると考えています。

Storybook について

私たちのテスト環境では、Storybook を使用してコンポーネントをレンダリングし、それを基に UI テストを行っています。そのため、基本的に全てのコンポーネントに Storybook を導入するようにしています。

Storybook を作成する際には、Scaffdog というツールを使用しています。このツールは、コンポーネントのファイルと同時に Storybook のファイルのテンプレートを作成することで、記述方法の統一や実装漏れを防ぐことができます。

Storybook に求めている役割を以下のようにしました。

  • 開発環境として
    Storybook 上で UI を実装・変更することにより、外部環境に影響を受けずに実装を行うことができる。
  • 品質の検査として
    Storybook 上でダークモードやライトモードでの表示の確認やアクセシビリティチェックを行い、品質の確認が行える。
  • ドキュメントとして
    各状態を Story として一覧で管理することにより、コンポーネントのドキュメントとして機能する。

コーディングルール

コーディングルールとしては、コンポーネントストーリフォーマット(CSF)に従って記述することと、Variant などの軽微な UI の違いはカスタムレンダリング機能を使用して、一つの Story として記述するようにしました。

const meta: Meta<Props> = {
  title: "ui/Chip",
  component: Chip,
  args: {
    children: "テキスト",
  },
};
export default meta;

const VariantTemplate: StoryFn<Props> = (args) => (
  <div className="p-4">
    <div className="pb-4">
      <p>primary</p>
      <Chip variant="primary">テキスト</Chip>
    </div>
    <div className="pb-4">
      <p>secondary</p>
      <Chip variant="secondary">テキスト</Chip>
    </div>
    <div className="pb-4">
      <p>tertiary</p>
      <Chip variant="tertiary">テキスト</Chip>
    </div>
  </div>
);

export const Variant: Story = {
  name: "Variant",
  render: VariantTemplate,
};

テスト観点

Story は以下の状態を基本的に揃えるようにしました。このようにすることで、Storyを見た時にある程度のコンポーネントの状態を理解できるようにしています。

  • 基本状態 (Basic): 全てのデータが揃っている、またはエラーが出ていない状態を表します。
  • 空の状態 (Empty): データがない状態を表します。
  • ローディング中の状態 (Loading): データを読み込み中の状態を表します。
  • 特別な表示状態: 特定の条件や機能によって表示が変わる状態を表します。

単体・結合テストについて

私たちは Vitest を使用して、単体テストと結合テストを行っています。

テストガイドラインには、コンポーネントの単体・結合テストを軸として記載しています。

単体・結合テストに求めている役割を以下のようにしました。

  • リグレッション耐性: 修正による意図せぬデグレを防ぐ。
  • リファクタリング耐性: 動作が保証された状態でコードを変更をできる。
  • 仕様の理解: テストを見ることで、ある程度の仕様を理解できる。

コーディングルール

テストの書き方に一貫性を出すためにコーディングルールを決めました。

  • AAA パターンを使用して記述する

Arrange-Act-Assert (AAA) パターンに従って、テストの流れを明示的に記述するようにしました。 これにより、読みやすいテストを書くことができると考えています。

test("...", async () => {
  // Arrange
  ...

  // Act
  ...

  // Assert
  ...
});
  • テスト名や説明は日本語で記述する

見やすさの観点からtestやdescribeは日本語で記述するようにしました。

test("正しい値が入力されている場合、入力した内容がサブミットされること", async () => {
  ...
});
  • クエリは Testing Library が推奨している優先度で使用する

クエリの優先順位は、Testing Libraryが推奨している下記の順位で使用することにしました。

この優先順位に従うことで、テストコードがユーザーの視点に近くなり、アクセシビリティの高いコードを書くことが出来ると考えています。

  1. 誰でもアクセスできるクエリ
    1. ByRole (基本的にこれを使用する)
    2. ByLabelText
    3. ByPlaceholderText
    4. ByText
    5. ByDisplayValue
  2. セマンティッククエリ
    1. ByAltText
    2. ByTitle
  3. テスト ID
    1. getByTestId
  • 要素への参照や入力、アクションは共通関数にまとめる

要素への参照や値の入力、ボタン操作などのアクションはまとめるようにしました。これにより、コードの再利用性が高まり、テストコードの可読性と保守性が向上すると考えます。

const objects = {
  emailField: () => screen.getByRole("textbox", { name: "メールアドレス" }),
  passwordField: () => screen.getByLabelText("パスワード"),
  submitButton: () => screen.getByRole("button", { name: "ログイン" }),
};

const actions = {
  inputEmail: (email: string) => userEvent.type(objects.emailField(), email),
  inputPassword: (password: string) =>
    userEvent.type(objects.passwordField(), password),
  onSubmit: () => userEvent.click(objects.submitButton()),
};

テスト観点

実装者ごとにテストの抜け漏れを防ぐために入れるべきテスト観点についても明記しました。

ただ、テストすべき観点は各コンポーネントによって変わるため、ここでは基本的に抑えるべきポイントのみを書きました。

Action

  • ユーザー操作の結果に応じた画面の変化
    ユーザーが行う操作に対して、アプリケーションが期待通りの反応を示すことを確認する。例えば、ボタンを押して開かれるモーダルやサイドビューの表示など。
  • フォームサブミット時に、適切な値が送信されること
    フォーム送信時に、ユーザーが入力したデータが正しく収集され、サーバーまたは次の処理に正しく渡されることを確認する。
  • ページナビゲーション
    リンクによるページナビゲーションが適切に行われるかを確認する。

Validation

  • フォームのバリデーションが適切に機能し、エラーメッセージがユーザーに表示されること
    入力値の検証が正しく行われ、不正な入力があった場合にはユーザーにわかりやすいエラーメッセージが表示されることを確認する。

また、テスト作成にかかる時間を考慮して、受け取ったPropsの値に基づいて画面が正しく表示されているかのテストについては、基本的に単体・結合テストではなく、スナップショットテストやビジュアルリグレッションテストを用いて、画面表示に変更がないことを確認するようにしました。

テスト名パターン

テスト名を揃えるために、大まかに命名のルールを設けました。

テスト名によってそのテストがどのような状態で何を確認するかがわかるように、状態と結果を明記するようにしました。

test("{状態}場合、{結果}こと", () => {

同じ状態に関連する複数のテストがある場合は、それらをdescribeブロックで囲むことで整理します。

describe("{状態}場合", () => {
  test("{状態}場合、{結果}こと", () => {
  };
  test("{状態}場合、{結果}こと", () => {
  };
}
describe("{状態}場合", () => {
  test("{結果}こと", () => {
  };
  test("{結果}こと", () => {
  };
}

ガイドラインを運用するためにやっていくこと

テストガイドラインを作っただけでは十分ではなく、うまく機能させるためには運用していくための工夫が必要だと考えました。

最初は、チームメンバーにテストガイドラインをレビューしてもらい、ガイドラインの認識の齟齬を無くしたりや、内容についても議論を行いました。

また、初期の段階は3週間のスプリントごとにテスト実装におけるモブプログラミングとフロントエンドテスト会議を実施したいと考えています。これにより、定期的にチームでテストに対しての考え方を揃えたりや、テストに対する知見の共有や議論が生まれると考えています。

テスト実装モブプログラミングについて

テスト実装のモブプログラミングでは、4人一組のチームでコンポーネントのテストを共同で作成します。各メンバーが15分ずつドライバー(実際にコーディングする役割)を担当し、合計60分で一つのテストを完成させます。

これにより、テストを書く際の知見の共有や、テストに対する共通の考え方を持てたりや、コンポーネントに対する責務の考え方なども議論することができます。

実際に実施してみたところ、アクセシビリティのRoleの確認方法やテストガイドラインの疑問点など、たくさんの気づきを得ることができました。

ここで出た意見を次のフロントエンドテスト会議でチーム全体で議論し、テストガイドラインに反映することができました。

フロントエンドテスト会議について

フロントエンドテスト会議では、モブプログラミングなどで出た意見を議論したりや、今後のテストの方針を決めたり、テストガイドラインの改善、そして次のスプリントでのテスト実装の進め方について話し合いました。

これまでテストに関してチーム全体で話し合う機会が十分になかったと感じていました。そこで、この会議を通じて既存のテストの課題点を議論したりや、今後のテスト実装の方向性を考える場になることを目指しています。

最後に

今回はフロントエンドチームのテストガイドラインを中心にご紹介させていただきました。

このガイドラインに関しては現在もチーム内で意見が出ており、改善の余地があると感じています。

MG-DXのフロントエンドチームでは、引き続きサービスの安定性と開発生産性を両立できる開発環境を目指していきたいと考えています。

最後までご覧いただきありがとうございました。

お気づきの点がございましたら@nobu_nobu_techまでご連絡ください。

 

テストガイドラインを作成するにあたって、参考にさせていただいた資料を記載します。