バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

バイセル Tech Blog

テスト文化の醸成に向けたフロントエンドチームの取り組み 〜Chromatic・Hygenの活用〜

はじめに

こんにちは!
2023 年 5 月にエンジニアとして入社したテクノロジー戦略本部 開発 3 部の辻岡です。

私は入社してから、顧客対応・SFA システム(以下、CRM)のフロントエンドの設計・開発に携わっています。

現在 CRM チームではフロントエンドのテストを書くことが当たり前になっています。

しかし、私が入社した当時は、キックオフからリリースまでの時間が限られていたことや、メンバーのフロントエンドテストに対する理解も高くなかったことから、フロントエンドのテストは書かれていませんでした。

今回の記事では、このような状況を改善し、テストを書く文化を醸成する過程で行った取り組みを紹介します。

なぜテストを書くのか

仕様を明確にするため

CRM チームのフロントエンドは複雑なシステムです。詳細は本筋ではないので割愛しますが、これには以下のような理由があります。

  • 業務フローの特殊性:CRM は、バイセル独自の業務フローに特化して設計されています。そのため標準的なライブラリや既存のベストプラクティスを適用することが難しいです
  • 機能の依存関係:一部機能が既存の CRM システムに依存しており、互換性を保つ必要があります
  • 事業部からの要求の変化:事業部の要求は常に変わり、仕様が固まった後でも変更されることがあります。CRM はこれらの変更に迅速に対応できる設計が求められます

上記の理由から社内システムにありがちな「動いているもの=仕様」といった状態に陥いるのではないかと考えました。そうなると、新規機能や機能改修時に仕様が不明確になり運用コストが嵩んでいきます。そこで、「テストケース=仕様」となるよう努め、テストを通して仕様を担保することを目指すことにしました。これにより、テストコードを見るだけで最新の仕様を把握でき、その仕様はテストによって担保されます。

リグレッションを検知するため

CRM チームは週に一度以上のリリースを目指しています。しかし、コードの変更が頻繁にプロダクション環境にデプロイされると、既存の機能が壊れるなどの原因で障害が発生する可能性が高まると考えました。この問題に対処するために、テストを充実させることでリグレッション(既存機能の退行)を事前に検知し、障害発生率を低減しながらデプロイの頻度を高めることを目指しました。

テストを書く上での課題

テストを書く方針は決まったものの、以下のような課題がありました。

  • フロントエンドメンバーの大部分がフロントエンドテストに精通していないこと
  • 今まで特にフロントエンドのテストを書く必要性を感じなかった・モチベーションが低かったこと
  • 時間がないこと

どのチームも同じような問題を抱えているのではないでしょうか。

やったこと

課題を踏まえ、とにかくテストを書く敷居を下げる取り組みをすることにしました。

開発者がビジュアルリグレッションテスト(以下 VRT)を意識しないで済む環境作り

先述したように、CRM チームでは週に一度以上のリリースを目指しており、リグレッション検知は必須です。特に見た目部分の変化はスナップショットテストだけでは気づけなかったり、目視で確認するのは非常にコストのかかる作業です。そこで VRT を導入し、さらに継続的にテストをコミットできるような環境を整えました。

開発者がすることは、React コンポーネントを作成する際に、pnpm new:fcとコマンドラインに入力するだけです。そうすると対話形式でファイルを配置する場所や、Storybook ファイルを作成するかを選択し、必要なファイルが生成されます。これで生成した React コンポーネントの VRT の準備は完了です。

コマンドの中身は、hygen を利用しています。以下フロントエンドチームでの使用例です。

ファイル生成に関するオプションを直感的に選べるコマンドラインを実装している箇所です。

module.exports = {
  prompt: ({ inquirer, _args }) => {
    const questions = [
      {
        type: "input",
        name: "partialPath",
        message: "どのディレクトリーに配置しますか?",
      },
      {
        type: "input",
        name: "name",
        message: "コンポーネント名は何ですか? ex: Button",
      },
      {
        type: "select",
        name: "fileName",
        message: "fileNameはなんですか?",
        choices: ["presenter", "container", "index"],
      },
      {
        type: "confirm",
        name: "requireStorybookFile",
        message: "storybookファイルは必要ですか?",
      },
      {
        type: "confirm",
        name: "requireTestFile",
        message: "テストファイルは必要ですか?",
      },
    ];
    return inquirer.prompt(questions).then((answers) => {
      const { name, partialPath, fileName } = answers;
      const path = `${partialPath}/${name}`;
      return { ...answers, path, fileName };
    });
  },
};

コマンドラインで入力した値を基にテンプレートから Storybook ファイルを生成します。

---
to: "<%= requireStorybookFile ? `${path}/${fileName}.stories.tsx` : null %>"
---
import { StoryObj } from "@storybook/react"

import { <%= name %> } from "./<%= fileName %>"

export default {
  component: <%= name %>,
  parameters: {
    chromatic: { disableSnapshot: false },
  },
} as StoryObj<typeof <%= name %>>

export const Primary: StoryObj<typeof <%= name %>> = {
  render: props => <<%= name %> {...props} />,
  args: {
  }
}

上記で実装したコマンドラインは以下のように実行されます。簡単にファイル生成できるインターフェースを実装できました。

CRM チームでは Chromatic を利用しているので、生成された Storybook ファイルは自動的にテスト対象となり、Chromatic 上で VRT が実行されます。

ここで注意したいことは、すべての React コンポーネントで Storybook ファイルを作成し、VRT の対象にしてしまうことです。そうすると CI の時間が長くなったり、Chromatic のような SaaS を利用している場合は料金が嵩んでしまいます。だからと言って無闇に減らすと、VRT の効果が落ちてしまいます。なので CRM のフロントエンドチームは以下のような React コンポーネントを VRT の対象にしています。

  • 複数の Story がある Storybook ファイル
  • ページ単位の Story

ページ全体を表す React コンポーネントのストーリーでは、個々のコンポーネントとページ全体のレイアウトを一緒にテストできます。また、複数のストーリーを持つ React コンポーネントは、特にテストの対象として明示的に選ぶ必要があります。このような方法で運用することにより、VRT の効果を最大限に引き出しつつ、同時にコストも抑えられます。

テストの準備コードは自動生成

テストを書く際には、テストの関心事となる部分以外にも、テストに必要なモジュールのインポートなどが必要です。これらの準備コードにも、hygen を活用します。

ここでは以下のような工夫をしています。

  • モジュールの種類に合わせて生成ファイルを変えること: CRM のフロントエンドチームでは主に、schema、単純関数(utils)、コンポーネント、hooks のモジュールでテストを書きます。しかしそれぞれのモジュールでテストに必要なコードは異なります。なのでpnpm new:hookpnpm new:schema,pnpm new:functionといったように細かく分けています
  • 生成した時点でコミットできる状態にすること: 生成した時点でテストが落ちていると、修正するのがコストになり次第にコマンドを使用してくれなくなる可能性があります

細かいことですが、こうした工夫があることでメンバーにコマンドの使用を促すことができました。

先例を作る

0-1 でテストを書く時、調べることがたくさんあります。

  • 外部通信の mock どうするんだっけ
  • ブラウザの API の mock どうするんだっけ
  • useEffect の実行を待ってから DOM を取得したいけど、どうしたらいいんだろ

そういったテストパターンを有識者が先導して実装します。そうすることでメンバーはテストパターンが近いテストコードを探して、書くときに参考にすることができます。

結果

これらの取り組みにより、テストの敷居を大幅に下げ、以下の好循環を生み出すことができました。

  1. テストを書く敷居が下がる
  2. テストを書く機会が増える
  3. テストを書いたことによる成功体験(デグレ検知など)を積む
  4. テストを書くモチベーションが上がる

そして、結果として以下のようにチーム全体でテストを充実させようとする文化を醸成することができています。

あくまで一つの指標としてですが、リリース 2 ヶ月時点のカバレッジレポートは以下です。

一般的に Statement Coverage は 80%~90%以上を目標とすることが多いです。 現状その数値を上回っているので、上出来ではないでしょうか。

終わりに

この記事では、フロントエンド開発におけるテスト文化の醸成に向けた私たちの取り組みを具体例として紹介しました。時間の制約やモチベーションの問題など、テストの実施に躊躇しているチームにとって、何かの参考になれば幸いです。

最後に、バイセルではエンジニアを随時募集しております。興味のある方はぜひ以下の採用サイトをご覧ください。

herp.careers