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

suzuki

こんにちは、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では、JavaScriptフレームワーク経験が豊富なパートナーさんを募集しています

Gaji-Laboでは、開発チームの一員としてプロジェクトに一緒に取り組んでくれる業務委託のパートナーさんを募集しています。

現在は特にJavaScriptフレームワーク実践と業務経験が豊富なWebフロントエンドエンジニアを必要としています。React + TypeScript、Vue.js、Next.js、Nuxt.js など、あなたの得意なフレームワークを教えて下さい!

パートナー契約へのお問い合わせもお仕事へのお問い合わせも、どちらもいつでも大歓迎です。まずはオンラインでのリモート面談からはじめましょう。ぜひお気軽にお問い合わせください!

お問い合わせしてみる!


suzuki

投稿者 suzuki

HTML/CSS のマークアップから始まり、現在は React や TypeScript を使ったコンポーネント実装をすることが多いです。

淡々と実装するだけではなくコミュニケーションを取りながら、チームとしてプロジェクトを前に進めることを意識しています。

最近は会社の成長へのコミットに関心があり、組織・チーム全体で強まるためにはどうするのだろう?ということを考えています。