😊

Storybookではじめる初めてのインタラクションテスト

に公開

はじめに

本記事は以下のような方向けに、Storybookを利用してインタラクションのテストを行う方法を紹介する記事になります!

  • テストの自動化を進めなきゃと思っているフロントエンドエンジニアの方
  • 画面側の自動テストってどんなことができるか気になっている方

前段

フロントエンド領域のテストは大きく以下の3つに分類されます。

  • 機能テスト
  • 非機能テスト
  • UI/UXテスト


引用元:https://zenn.dev/silverbirder/articles/c3de04c9e6dd58

ユニットテスト等はすでに沢山の本や記事が出ているため、今回はインタラクションにフォーカスを当てて考えていきます。

Step1:テストの対象を決める

では早速テストに取り掛かっていく...前に、テストの対象となるコンポーネントを選定します。
メールアドレスを入力するシンプルなコンポーネントに対しテストを作成していこうと思います。

デジタル庁デザインシステムを参考に、以下仕様のコンポーネントを用意しました。

  • 入力の最大文字数は20文字
  • メールアドレスが正しくない形式の場合(@・ドットがない等)はエラーを出す

Input.tsx
import { useState } from 'react'

type InputProps = {
  type: 'email' | 'input' | 'password'
  value: string
  placeholder: string
  state: 'default' | 'error' | 'disabled' | 'readonly'
  size: 'large' | 'medium' | 'small'
  errortext?: string
}

export const Input = ({ type, value, placeholder, state, size, errortext }: InputProps) => {
  const [inputValue, setInputValue] = useState(value)
  const [inputState, setInputState] = useState(state)
  const [inputErrortext, setErrortext] = useState(errortext)

  const isValidEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)

  const handleChangeInput = (e) => {
    const value = e.target.value
    setInputValue(value)
    if (value === '') return

    // Emailのバリデーション
    if (type === 'email' && !isValidEmail(value)) {
      setInputState('error')
      setErrortext('メールアドレスを正しい形式で入力してください。')
    }
  }

  return (
    <div className={`c-input c-input--${size} c-input--${state}`}>
      <input
        maxLength={20}
        type={type}
        placeholder={placeholder}
        value={inputValue}
        onChange={handleChangeInput}
      ></input>
      <span className="c-input__background"></span>
      {inputState === 'error' && <p className="c-input__errortext">{inputErrortext}</p>}
    </div>
  )
}

Step2:Storyをつくる

テストの内容にあわせて、以下のStoryを作成します。

  • 最大文字数
  • 誤った形式でメールアドレスを入力した場合
Input.story.ts
import { Input } from '../components/ui-elements/input'
import './../common/styles/_index.scss'


// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
export default {
  title: 'ui-test/Input',
  component: Input,
  parameters: {
    // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
    layout: 'centered',
  },
  // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
  tags: ['autodocs'],
  // More on argTypes: https://storybook.js.org/docs/api/argtypes
  argTypes: {
    type: { control: 'select', options: ['email', 'input', 'password'] },
    state: { control: 'select', options: ['default', 'error', 'disabled', 'readonly'] },
    size: { control: 'select', options: ['large', 'medium', 'small'] },
  },
}

export const 初期状態 = {
  args: {
    type: 'email',
    value: '',
    placeholder: '値を入力してください',
    state: 'default',
    size: 'large'
  }
}


export const 最大文字数の入力 = {
  args: {
    type: 'email',
    value: 'この文字はサンプルテキストです。20文字',
    placeholder: '値を入力してください',
    state: 'email',
    size: 'large',
    errortext: ''
  }
}

export const 誤った形式でメールアドレスを入力した場合 = {
  args: {
    type: 'email',
    value: 'sample.comcom',
    placeholder: '値を入力してください',
    state: 'error',
    size: 'large',
    errortext: 'メールアドレスを正しい形式で入力してください。'
  }
}

Step3: テストコードを記述する

インタラクションテストでは、ユーザーの操作をテストコード上で行い、操作後のDOMの状態が正しいか確認をします。

  • 最大文字数
    最大文字数のストーリーでは、21文字以上を入力したときに、20文字以上は切り取られることを確認します。
export const 最大文字数の入力 = {
  args: {
    type: 'email',
    value: '',
    placeholder: '値を入力してください',
    state: 'email',
    size: 'large',
    errortext: ''
  },
  play: async ({ canvasElement, args }) => {
    // 1. 要素を取得するための canvas を定義
    const canvas = within(canvasElement)

    // 2. input 要素を取得
    const inputElement = canvas.getByPlaceholderText(args.placeholder)

    // 3. 25文字の長い文字列を準備
    const longText = 'あいうえおかきくけこさしすせそたちつてとなにぬねの' // 25文字
    const expectedText = 'あいうえおかきくけこさしすせそたちつてと' // 20文字

    // 4. 25文字分をタイピング(しかしmaxLengthにより20文字で止まるはず)
    await userEvent.type(inputElement, longText)

    // 5. 検証:入力値が20文字で切り捨てられているか
    expect(inputElement).toHaveValue(expectedText)
    expect(inputElement.value.length).toBe(20)

    // 4. (オプション) 21文字目が入力されていないことを念押しで確認
    expect(inputElement.value).not.toBe(longText)
  },
}
  • 誤った形式でメールアドレスを入力した場合
    sample.com.comと入力して、エラーになることを再現します。
export const 誤った形式でメールアドレスを入力した場合 = {
  args: {
    type: 'email',
    value: '',
    placeholder: '値を入力してください',
    state: 'default',
    size: 'large',
    errortext: 'メールアドレスを正しい形式で入力してください。'
  },
  play: async ({ canvasElement, args }) => {
    // 1. 要素を取得するための canvas を定義
    const canvas = within(canvasElement)

    // 2. input 要素を取得
    const inputElement = canvas.getByPlaceholderText(args.placeholder)

    // 3. ユーザー操作: 文字を入力
    await userEvent.type(inputElement, 'sample.com.com')

    // 4. 検証: エラーメッセージが表示されるまで待機
    await waitFor(() => {
      const errorText = canvas.getByText('メールアドレスを正しい形式で入力してください。')
      expect(errorText).toBeInTheDocument()
    })

    // 5. 検証: クラス名が変化しているか
    // inputElement の親要素(.c-input)を特定して検証
    const container = inputElement.closest('.c-input')
    expect(container).toHaveClass('c-input--error')
  },
}

Step4. テスト結果を確認する

テストコードが書けたら、 テストを実行していきます!
Storybookからテストするコンポーネントを選択したら、Component Testsのタブを開きます。
テストコードにかいたそれぞれの動作に対し、コンポーネントが期待値通り動いていることが確認できます。

おわりに

いかがだったでしょうか。
今回ははじめの第一歩として、Inputのコンポーネントに対しテストを実施しました。
応用すれば、フォーム全体のインタラクションテストなどを自動化することができると感じました。

みなさまも引き続きよきテストライフをお送りください。

謝辞

この記事は以下を参考にいたしました。お礼申し上げます。

Storybook公式
https://storybook.js.org/tutorials/intro-to-storybook/react/ja/test/
https://storybook.js.org/docs/writing-tests/interaction-testing

Webフロントエンドにおける網羅的テストパターンガイド
https://zenn.dev/silverbirder/articles/c3de04c9e6dd58

Storybookで始めるUIコンポーネントのインタラクションテスト
https://note.shiftinc.jp/n/neec5650c805d

GitHubで編集を提案

Discussion