React コンポーネントのテストを書く時に考えることと、テストコードサンプル


こんにちは、Gaji-Labo フロントエンドエンジニアの鈴木です。
普段 React + TypeScript でコンポーネントを実装する時に、テストコードを書くようにしているのですが、その際に考えていることと、実際のテストコードについて書いていきます。

テストの必要性について

以前に「テストコード書いた方がいいよね、でもテストするってどういうことだろう?」という内容のブログを書きました。
今回はそのような内容には触れませんが、もし興味があれば「コンポーネント開発でテストを書くための意識作り」も合わせてご覧ください。

ではさっそく、実際にどのようなテストを書いているのか掘り下げていきたいと思います。

なにをテストするか

コンポーネントがあった方が分かりやすいので、以下のボタンコンポーネントを例として進めていきます。

interface Props {
  className?: string;
  isDisabled?: boolean;
  isSmall?: boolean;
  onClick?: () => void;
  label: string;
}

const SomethingButton: FC<Props> = ({
  className,
  isDisabled,
  isSmall,
  onClick,
  label,
}) => {
  const classes = classNames(
    css.SomethingButton,
    { [css.isSmall]: isSmall },
    className,
  );
  return (
    <button
      className={classes}
      disabled={isDisabled}
      onClick={onClick}
      type="button"
    >
      {label}
    </button>
  );
};

このようなコンポーネントがあった場合、以下の内容をテストしたいと考えます。

  • コンポーネントの役割が意図通り機能しているか
  • スタイルの変化を持つ場合、それが適用されているか

それを具体的なテスト方法に落とし込むと以下のようになります。

  • label のテキストが表示されているか
  • onClick に渡した関数が呼ばれるか
  • isDisabledtrue の時に disabled 属性が渡っているか
  • isSmalltrue の時に クラス名 .isSmall が付与されているか

それぞれのテストの書き方

スナップショットテストとユニットテストを使い分けて、それぞれテストしていきます。

スナップショットテスト

テストする項目

  • label のテキストが表示されているか

テストをするポイント

  • テキストが表示されていることが分かれば OK

スナップショットテストで全テストをカバーするのではなく、コンポーネントのレンダリング結果が意図通りであることを確認できれば十分だと考えています。

import { shallow } from 'enzyme';
import * as React from 'react';

import SomethingButton from '../SomethingButton';

describe('<SomethingButton />', () => {
  it('renders correctly', () => {
    const wrapper = shallow(<SomethingButton label="button label" />);
    expect(wrapper).toMatchSnapshot();
  });
});

ユニットテスト

テストする項目

  • onClick に渡した関数が呼ばれるか
  • isDisabledtrue の時に disabled 属性が渡っているか
  • isSmalltrue の時に クラス名 .isSmall が付与されているか

いくつかあるので一つずつテストコードを書いていきます。

onClick に渡した関数が呼ばれるか

describe('onClick', () => {
  it('should calls', () => {
    const mockOnClick = jest.fn();
    const wrapper = shallow(
      <SomethingButton label="button label" onClick={mockOnClick} />,
    );
    wrapper.simulate('click');
    expect(mockOnClick).toHaveBeenCalled();
  });
});

このテストではモック関数を使用しています。
toHaveBeenCalled を使用することでモック関数が呼ばれたかを確認できます。

参考リンク

isDisabledtrue の時に disabled 属性が渡っているか

describe('with [disabled=true]', () => {
  it('should have `disabled` attribute', () => {
    const wrapper = shallow(
      <SomethingButton label="button label" isDisabled={true} />,
    );
    expect(wrapper.find('.SomethingButton').prop('disabled')).toBeTruthy();
  });
});

prop('disabled') を使用して disabled 属性が渡っているかをテストします。

参考リンク

isSmalltrue の時に クラス名 .isSmall が付与されているか

describe('with [isSmall=true]', () => {
  it('should have `.isSmall`', () => {
    const wrapper = shallow(
      <SomethingButton label="button label" isSmall={true} />,
    );
    expect(wrapper.find('.SomethingButton').hasClass('isSmall')).toBeTruthy();
  });
});

hasClass を使用して CSS クラス名 isSmall が存在することをテストします。

参考リンク

テスト全体で意識していること

どのテストでも共通して意識しているポイントは以下の2つです。

テストに必要のないオプショナルな props は渡さない

例えば onClick に渡した関数が呼ばれるかのテストで isSmall を渡さない、といった感じです。テストに必要ないものは渡さず、テストしたいものだけを持っている状態にします。

何がテストされたいか分かるようにする

テストコードを書いている本人はテストしたい内容を分かっているので問題ありませんが、誰が読んでも「このテストでなにを期待しているか」が分かるように意識しています。

まとめ

普段どのようにコンポーネントのテストを書いているのか掘り下げてみました。
なにをどのようにテストするのか、1つずつ噛み砕いていくと、それぞれのテストもコンパクトに書けると思います。
また、どうテストするかのパターンをたくさん知っているとテストしやすいと感じています。他の人の書いたテストコードを見るのは発見も多く、「そうかけるのか〜!」と見ていて楽しいです。

Gaji-Labo は React 開発の実績と知見があります

フロントエンド開発の専門家である私たちが御社の開発チームに入ることで、バックエンドも含めた全体の開発効率が上がります。

「人手が足りず信頼できるエンジニアを探している」
「Vue.js から React へリプレイスを検討している」
「フロントエンドの効率化をどうしたらいいか分からない」
「自分たちで手を付けてみたがいまいち上手くいかない」

フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にご相談ください。

オンラインでのヒアリングとフルリモートでのプロセス支援にも対応しています。

フロントエンドの相談をする!

投稿者 Gaji-Labo Staff

Gaji-Laboの社内デジタル環境でいろいろなお手伝いをしているがじ専務&じら常務。みんなのシリーズ記事をまとめたり、卒業したスタッフの過去記事を記録したり、Twitterをやったりしています。