【Insight BRAIN】フロントエンドのテスト実施状況

はじめに

皆さんはじめまして、 オープンエイト・プロダクト開発部の村田です。時が経つのは早いもので、知らない間に入社2年目となっていました。

それはさておき、今回はオープンエイト が提供するSNSの分析ツール「Insight BRAIN」において実施されている、フロントエンドのテストについて記事を書かせていただきました。プロダクトの品質を維持するために、フロントエンドでどのような取り組みを行ってきたのか、本記事では余すところなく解説させていただきます。

Insight BRAINとは?

f:id:kaimaru31:20210715151432p:plain

オープンエイト が提供するAIで動画を作成できるツールVideo BRAINに対して、Insight BRAINは企画と分析領域をサポートし、SNS等に投稿した動画に対しての効果測定、また更なる企画立案を可能にしてくれるものです。

Insight BRAINではバックエンドはScalaRuby、フロントエンドはTypeScriptとReactを採用し、マイクロサービスとしてそれぞれで開発を行っています。

Insight BRAINで行われているテスト内容

Insight BRAINのフロントエンドで現状行われているテストとして、主に以下のようなコンポーネント単位での単体テストを行っています。

スナップショットテスト

コンポーネントに特定のpropsを渡した時に生成されるDOMを作成しておいたsnapshotデータと比較するテスト。 これにより、周辺のコード変更があってもコンポーネントのUIが変化していないことを担保できる。

イベント発火テスト

コンポーネントの動作をシミュレートすることでイベントやその関数が呼ばれていることを確認するテスト。 これにより、周辺のコード変更があっても該当箇所に関して正しくイベント発火することを担保できる。

関数の挙動テスト

作成した関数に対して、テストデータを与えて正しい出力となるか確認するテスト。

これらの単体テストを行うため、Insight BRAINではJestとEnzymeを用いてコンポーネントごとにテストを行っています。

Jest

Facebookが提供しているJavaScriptのテスティングフレームワーク

Enzyme

Airbnbが提供しているReactのテスティングライブラリ

テストファイルは以下のようにsrc配下の__test__ディレクトリに振り分けられ、コンポーネントごとのテストが容易になっています。

src/
  ├ components/
  |    └ componentFiles.../ componentName.tsx
  ├ pages/
  |    └ componentFiles.../ componentName.tsx
  └ __test__/
       └ components/ componentFiles.../ componentName.test.tsx
       |_ pages/ componentFiles.../ componentName.test.tsx

具体的なテストコードの内容

では、具体的にはどのようなテストコードを書いているのか説明します。

例として、投稿内容を表示するPostModalというコンポーネントについて書かれたスナップショットテスト及び、イベントの発火テスト、また関数の挙動のテストについて解説します。

①通常(データが正しくある場合)のスナップショットテスト

import React from "react";
// enzymeに関するインポート
import { shallow } from "enzyme";
import EnzymeToJson from "enzyme-to-json";
// Component自体のインポート
import PostModal from "src/Components/Elements/PostModal/";
// APIで取得されるはずのモックデータのインポート
import { account } from "src/__tests__/mock/sns_accounts/{sns_account_id}";
import { post } from "src/__tests__/mock/{sns_account_id}/posts/{post_id}";

// ①通常(データが正しくある場合)のスナップショットテスト
test("<PostModal />+データがある場合のスナップショット", () => {
// shallow renderingとして子コンポーネントの振る舞いに関わらずテストする。
  const subject = shallow(
    <PostModal
      modalVisible={true}
      snsAccount={account}
      postContentData={post}
// jest.fn()でモックの関数を発火できる。
      handleModalVisible={jest.fn()}
    />
  );
  expect(EnzymeToJson(subject)).toMatchSnapshot();
});

ここでやっていることは、PostModalコンポーネントをshallow renderした結果をsubjectに代入し、 EnzymeToJsonでJestのテスティング関数であるexpectの引数に合うような形に変換。その結果をtoMatchSnapshot()で、既存のスナップショットテストと相違があるかどうか確認しています。 このテストを実行すると、snapshotsファイルが生成され、テスト実行時点でのDOMがアウトプットされるとともに、以前取得したsnapshotの結果と相違がある場合、エラーとして通知されます。

②データがnullの場合のスナップショットテスト

// import 省略
// ②データがnullの場合のスナップショットテスト
test("<PostModal />+データがnullでのスナップショット", () => {
  const subject = shallow(
    <PostModal
      modalVisible={true}
      snsAccount={account}
      postContentData={null}
      handleModalVisible={jest.fn()}
    />
  );
  expect(EnzymeToJson(subject)).toMatchSnapshot();
});

このテストも①と同じスナップショットテストですが、何らかの理由でデータがnullになっていることを想定したテストです。このようにして、テストのカバレッジ率を高めようとしています。

③イベントの発火テスト

// import 省略
// ③イベントの発火テスト

test("<PostModal />+handleModalVisibleの発火テスト", () => {
  const testHandleModalVisible = jest.fn();
  const subject = shallow(
    <PostModal
      modalVisible={true}
      snsAccount={account}
      postContentData={post}
      handleModalVisible={testHandleModalVisible}
    />
  );
  subject
    .dive()
    .find("ModalImage")
    .simulate("click");
  expect(testHandleModalVisible).toHaveBeenCalled();
});

続いてイベントの発火テストです。subjectという変数にレンダリングした結果を入れているのは①、②と同じですが、ModalImageというコンポーネントが持つonClickの関数を発火させたかったため、その階層までdiveという機能を使ってDOMの階層を移動しています。その上でsimulate("click")で関数を発火させ、expectに渡したtestHandleModalVisibleが呼ばれているか(toHaveBeenCalled)テストしています。このテストを行い、該当コンポーネントでの関数の発火に失敗するとエラーとして通知されます。

④関数の挙動テスト

import { omitNumberOfDigits } from "src/Modules/OmitNumberOfDigits";
// ④関数の挙動テスト

test("Omit number of digits. Return omitted number that is string type.", () => {
  expect(omitNumberOfDigits(300)).toEqual("300");
  expect(omitNumberOfDigits(1340)).toEqual("1.3K");
  expect(omitNumberOfDigits(1599)).toEqual("1.5K");
  expect(omitNumberOfDigits(10000)).toEqual("10.0K");
  expect(omitNumberOfDigits(1_470_000)).toEqual("1.4M");
});

最後は関数に関するJestのテストです。 omitNumberOfDigitsという、引数として与えられた数字の大きさに応じて「K」, 「M」と言った単位を付与する関数について、正しい結果をアウトプットしているかをテストしています。 expectに関数の返り値を与え、toEqualで意図した結果と等しいかを検証しています。

社内では今回紹介したような単体テストを引き続き行っていくだけでなく、今後複数コンポーネントをまたぐ結合テストや、視覚的に描画結果を確認できるビジュアルテストなども検討し、テストを強化していきたいと考えています。

おわりに

これからもInsight BRAIN、Video BRAINともにますます機能を追加していき、皆様に満足いただけるサービスとなるよう社員一同、精進していきたいと思っています。 開発で得られた知識をブログで還元できるよう、個人としても精進していきます!