Storybookではじめる初めてのインタラクションテスト
はじめに
本記事は以下のような方向けに、Storybookを利用してインタラクションのテストを行う方法を紹介する記事になります!
- テストの自動化を進めなきゃと思っているフロントエンドエンジニアの方
- 画面側の自動テストってどんなことができるか気になっている方
前段
フロントエンド領域のテストは大きく以下の3つに分類されます。
- 機能テスト
- 非機能テスト
- UI/UXテスト

引用元:https://zenn.dev/silverbirder/articles/c3de04c9e6dd58
ユニットテスト等はすでに沢山の本や記事が出ているため、今回はインタラクションにフォーカスを当てて考えていきます。
Step1:テストの対象を決める
では早速テストに取り掛かっていく...前に、テストの対象となるコンポーネントを選定します。
メールアドレスを入力するシンプルなコンポーネントに対しテストを作成していこうと思います。
デジタル庁デザインシステムを参考に、以下仕様のコンポーネントを用意しました。
- 入力の最大文字数は20文字
- メールアドレスが正しくない形式の場合(@・ドットがない等)はエラーを出す

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を作成します。
- 最大文字数
- 誤った形式でメールアドレスを入力した場合
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公式
Webフロントエンドにおける網羅的テストパターンガイド
Storybookで始めるUIコンポーネントのインタラクションテスト
Discussion