本連載について
はじめまして! サイボウズ フロントエンドエキスパートチームのnus3です。
本連載では、Webフロントエンドに関してもう一歩踏み込んだ知識について、サイボウズフロントエンドエキスパートチームのメンバーによって不定期で解説記事を掲載しています。
本記事では、モダンフロントエンドにおけるテストについて、その種類や導入方法などを紹介します[1]。
モダンフロントエンドでのテスト
ここ数年のWebフロントエンドでは次のように、さまざまな変化がありました。
- ReactやVue.
jsといった宣言的UIを採用したライブラリの普及 - TypeScript中心としたエコシステムの発展
- ES Modulesの採用の広がり
- ViteやSWC、RspackやTurbopackなどの新しいビルドツールの登場
この変化に合わせて、Webフロントエンドを対象にしたテストのツールや手法も増えてきました。
特に筆者が大きく変わったと感じているのは、UIコンポーネントを対象にしたツールや手法が増えた点です。これは、宣言的UIを採用したライブラリの普及によって、コンポーネントベースでの開発が主流になったことが大きな要因だと考えています。テスト対象にUIコンポーネントが増えたことで、プロダクトやプロジェクトの状況に合わせたテスト手法の幅が広がりました。
また、Vitestのようにビルドツールと同じ条件でテストを実行できるテストフレームワークや、DenoやBunのようにテストを実行する機能をビルトインで持つランタイムが登場しました。これにより、テストのための環境構築を整えるコストも大きく軽減され、テストを導入するハードルは下がったように感じています。
本記事では、これらのモダンフロントエンドの変化を踏まえ、具体的に次のようなケースのテストを取り上げていきます。
- 主なテストフレームワークでのテスト例
- UIコンポーネントを対象にしたテスト
主なテストフレームワークでのテスト例
Webフロントエンドの開発では、TypeScriptが一般的に使われるようになってきました。TypeScriptで書かれたコードは、事前にJavaScriptにトランスパイルされ、Node.
簡単な数値を加算する関数と、そのテストを例に、各フレームワークの特徴を確認してみましょう。
Jest
JestはBabelやTypeScript、ReactやVueといった多くのライブラリに対応したテストフレームワークです。npm trendsを見ると、ライブラリの更新頻度は少なくなってきたものの、長く運用され、多くのプロジェクトでダウンロードされていることがわかります。

JestはデフォルトだとNode.
Jestの設定にはtransform
オプションがあり、対応するライブラリ
指定できるライブラリには次のようなものがあります。
次の関数とテストを例に、それぞれの導入方法や特徴を見ていきましょう。
export function add(a: number, b: number): number {
return a + b;
}
import { add } from "./calculator";
test("引数に渡した二つの数値を合算した値を返す", () => {
expect(add(1, 2)).toBe(3);
expect(add(-1, 1)).toBe(0);
expect(add(5, 5)).toBe(10);
});
package.
にはJestを実行するためのnpm scriptsを追加しておきましょう。
{
"scripts": {
"test": "jest"
}
}
Babel
BabelにはJest用のプラグインbabel-jest
)@babel/
)
@babel/
ではテスト実行時に型チェックは行われないので、型チェックを行いたい場合は、別途TypeScriptのコンパイラtsc
)
導入方法としては、まず必要なライブラリをインストールします。
# JestとTypeScriptのインストール
npm install -D typescript jest @types/jest
# Babelに必要なライブラリのインストール
npm install -D @babel/core @babel/preset-env @babel/preset-typescript babel-jest
次に、Babelの設定ファイルbabel.
)
{
"presets": [
["@babel/preset-env", { "targets": { "node": "current" } }],
"@babel/preset-typescript"
]
}
最後にJestの設定ファイルjest.
)transform
にbabel-jest
を指定することで、Jest実行時にBabelを使ってTypeScriptのトランスパイルをします。
/** @type {import('jest').Config} */
module.exports = {
transform: {
"^.+\\.ts$": "babel-jest",
},
};
ts-jest
ts-jest
はJestで使えるTypeScript用のトランスフォーマーです。ts-jest
はBabelの@babel/
とは異なり、テストの実行時に合わせて型チェックを行うのが特徴です。
導入方法としては、まず必要なライブラリをインストールします。
# JestとTypeScriptはインストールしている前提
npm install -D ts-jest
次にJestの設定ファイルjest.
)transform
にts-jest
を指定することで、Jest実行時にTypeScriptのトランスパイルをします。
/** @type {import('jest').Config} */
module.exports = {
transform: {
"^.+\\.ts$": "ts-jest",
},
};
@swc/jest
@swc/
はSWCをトランスパイラーとして使うJest用のトランスフォーマーです。SWCはRustで書かれたコンパイラーで、高速な処理が特徴であり、公式ドキュメントにも次のように書かれています。
SWC is 20x faster than Babel on a single thread and 70x faster on four cores.
この記載のように、SWCをベースにした@swc/
はトランスパイル速度の向上により、Babelやts-jest
に比べてテストの実行時間が短縮されることが期待できます。
また、@swc/
は@babel/
と同様に、テストの実行時に型チェックを行いません。
導入方法としては、まず必要なライブラリをインストールします。
# JestとTypeScriptはインストールしている前提
npm install -D @swc/core @swc/jest
次にJestの設定ファイルjest.
)transform
に@swc/
を指定することで、Jest実行時にSWCを使ってTypeScriptのトランスパイルをします。
/** @type {import('jest').Config} */
module.exports = {
transform: {
"^.+\\.ts$": "@swc/jest",
},
};
テスト実行時間の比較
同じテストを対象に、Babel、ts-jest
、@swc/
を使って手元でテストを実行すると、それぞれ実行時間は次のようになりました。
トランスフォーマー | 実行時間 |
---|---|
Babel | 8. |
ts-jest |
7. |
@swc/ |
1. |
環境や条件によって実行時間は変わると思いますが、@swc/
の特徴にあるように、ほか2つのトランスフォーマーに比べてテストの実行時間が短縮されていることがわかります。
Vitest
Vitestは、フロントエンドのビルドツールであるViteネイティブなテストフレームワークです。
また、後発なテストフレームワークということもあり、Vitestでは、Jestと互換性のあるAPIの提供や、ES Modulesファースト、TypeScriptやJSXのサポート、ブラウザ上でテストが実行できるBrowser Modeといった昨日や特徴があります。
Jestと比較した際にVitestは後発のテストフレームワークであり、Jestの良い点を継承しつつ、TypeScriptサポートやESM対応といった課題であった点が解消されていることなどから、Vitestを採用するケースも増えてきているように感じています。npm trendsからもViteを採用するプロジェクトが増えていることが伺えます。
Vitest内部でViteに依存しており、ViteではTypeScriptのトランスパイルが機能として組み込まれているので、TypeScriptで書かれた関数をテストする場合のみであれば、特に設定を行う必要はありません。
導入方法としては、必要なライブラリをインストールするだけです。
npm install -D typescript vitest
Vitestのデフォルトでは.test.
か.spec.
がファイル名に含まれるファイルをテストファイルとして認識するので、calculator.
ファイルを作成します。
import { test, expect } from "vitest";
import { add } from "./calculator";
test("引数に渡した二つの数値を合算した値を返す", () => {
expect(add(1, 2)).toBe(3);
expect(add(-1, 1)).toBe(0);
expect(add(5, 5)).toBe(10);
});
最後にテスト用のnpm scriptsを追加して実行することで、TypeScriptで書かれた関数をVitestでテストできます。
{
"scripts": {
"test": "vitest"
}
}
Rstest
余談にはなりますが、Vitestと同様にビルドツールネイティブなテストフレームワークとして、Rstestの開発[3]が進められています。
GitHubのリポジトリにあるREADMEに記載されている内容を読むと、次のような特徴があるようです。
- Rspackをベースとしたテストフレームワーク
- Jest互換のAPIを提供
- TypeScriptやES Modulesのネイティブサポート
RspackはRustで実装されたバンドラーで、webpackとの高い互換性が特徴です。
Rstestはまだ開発段階ですが、ViteとVitestのように、Rspackを使ったアプリケーションに対して、同じ条件でテストを実行できるテストフレームワークとして、今後の動向が楽しみです。
ロードマップには2025年6月中にプレビューバージョンをリリースし、2025年中には正式リリースを計画していることが記載されています。
DenoとBun
DenoとBunのように、テストランナーをビルトインで持つランタイムも登場しています。どちらもTypeScriptをネイティブでサポートしています。
これら2つのランタイムでは、どちらもTypeScriptをネイティブでサポートしています。また、テストを実行するのにライブラリのインストールや設定は不要で、それぞれbun test
やdeno test
を実行するだけでTypeScriptの関数をテストできます。
Denoでは、Deno.
を使うことでテストを定義できます。
// Denoの標準ライブラリから、テスト用のアサーション関数をインポート
import { assertEquals } from "@std/assert";
// Denoの標準ライブラリから、Jestライクのexpect APIをインポート
import { expect } from "jsr:@std/expect";
import { add } from "./main.ts";
// Deno.testでテストケースを定義し、アサート
Deno.test("add", () => {
assertEquals(add(2, 3), 5);
});
Deno.test("add", () => {
const result = add(2, 3);
// Jestのような記法も利用できる
expect(result).toBe(5);
});
Bunでは、Jest互換のテストランナーがビルトインされており、定義方法もJestと同様の記法が使えます。
// Bun組み込みのアサーション関数をインポート
import { expect, test } from "bun:test";
import { add } from "./main.ts";
// Jest互換のtest関数でテストケースを定義
test("add", function addTest() {
expect(add(2, 3)).toBe(5);
});
UIコンポーネントを対象にしたテスト
ReactやVue.
ここでは、次のようなReactで実装したフォームコンポーネントを例に、どういったライブラリを使い、どのようなテストができるか見ていきましょう。
import { type FormEvent, useState, type FC } from "react";
export type FormProps = {
onSubmit: (name: string) => void;
};
export const Form: FC<FromProps> = ({ onSubmit }) => {
const [name, setName] = useState("");
// Name変更時のイベントハンドラー。入力値をstateに保持する
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
// サブミット時のイベントハンドラー。入力値をonSubmitに渡す
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
onSubmit(name);
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Name</label>
<input id="name" type="text" value={name} onChange={handleChange} />
<button type="submit">送信</button>
</form>
);
};
Node.js環境でUIコンポーネントをテストする
JestやVitestなどのテストフレームワークとjsdom
やhappy-dom
を組み合わせることで、Node.
jsdom
やhappy-dom
といったライブラリでは、DOMやHTMLなどのブラウザ上で実行できるWeb APIをエミュレートします。これによりNode.
実際に、Vitestとjsdom
を組み合わせて、Reactで実装したフォームコンポーネントをテストしてみましょう。
Vitestでjsdomを使う
Vitestではテストで使用する環境をenvironment
オプションで指定できます。デフォルトではNode.jsdom
やhappy-dom
、edge-runtime
の環境を指定できます。
導入方法としては、まず必要なライブラリをインストールします。
# React関連
npm install react react-dom
npm install -D @types/react @types/react-dom
# Vite関連
npm install -D vite @vitejs/plugin-react
# Vitestとテスト関連
npm install -D vitest @testing-library/react jsdom @testing-library/user-event
Viteの設定ファイルであるvite.
を作成し、Reactプラグインの追加と、Vitestでのテストの実行にjsdom
を指定します。
/// <reference types="vitest" />
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
// Reactプロジェクト用のViteプラグインを追加
plugins: [react()],
test: {
// describe、test、expectなどをインポートせずに(グローバルAPI)使用するかどうか
globals: true,
// テストに使用する環境
environment: "jsdom",
// テスト実行前に実行されるセットアップファイルを指定
setupFiles: "./setup.ts",
},
});
@testing-library/
を使う場合、Vitestのglobals
オプションを有効にすることで、自動的にcleanupされます。globals
オプションを使わずに次のようにテスト実行後にcleanupを行う処理をsetupFiles
に追加することで同様のことができます。そのため、globals
オプションを使うかどうかは、ドキュメントに記載されているとおり、グローバルなAPIとして扱うかどうかプロジェクトの方針に合わせて選択するのが良いでしょう。
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
afterEach(() => {
cleanup();
});
次にテストファイルを作成し、フォームコンポーネントに対して次のようなテストを実行します。
- フォームコンポーネントを描画
- name用のinput要素に値を入力
- 送信ボタンをクリック
- submit時に入力された値が渡されていることを確認
import { describe, test, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Form } from "./Form";
describe("Form", () => {
test("フォームに名前を入力して送信ボタンをクリックすると、onSubmitが呼ばれる", async () => {
const onSubmit = vi.fn();
// 1. フォームコンポーネントを描画
render(<Form onSubmit={onSubmit} />);
// 2. name 用の input 要素に値を入力
const nameField = screen.getByRole("textbox", { name: "Name" });
await userEvent.type(nameField, "nus3");
// 3. 送信ボタンをクリック
const submitButton = screen.getByRole("button", { name: "送信" });
await userEvent.click(submitButton);
// 4. submit 時に入力された値が渡されていることを確認
expect(onSubmit).toHaveBeenCalledWith("nus3");
});
});
このように、Vitestとjsdom
を組み合わせることでNode.
JSXのトランスパイル
Reactでは、UIコンポーネントを実装する際にJavaScriptの拡張であるJSXを使います。これまで紹介してきたJestやVitestといったテストフレームワークでJSXを対象にテストを実行する場合、JSXをJavaScriptにトランスパイルする必要があります。
Vitestでは、ベースにあるViteでJSXのトランスパイルがサポートされており、さらにVite公式のプラグインである@vitejs/
を組み合わせて使うことで、テスト実行時にもReactに適したビルドを行います。
Jestでは、本記事で紹介した次の3つのトランスフォーマーでそれぞれJSXのトランスパイルをサポートしています。
- Babel(
@babel/
)preset-react ts-jest
@swc/
jest
ブラウザ環境でUIコンポーネントをテストする
jsdom
やhappy-dom
といったライブラリを使い、Node.
たとえば、次のようにまったく同じ位置に2つのボタンが重なって配置されているコンポーネントがあるとします。
import { type FC } from "react";
export type OverlappingButtonsProps = {
// 背面ボタンをクリックしたときのイベントハンドラ
onBackButtonClick: () => void;
// 前面ボタンをクリックしたときのイベントハンドラ
onFrontButtonClick: () => void;
};
export const OverlappingButtons: FC<OverlappingButtonsProps> = ({
onBackButtonClick,
onFrontButtonClick,
}) => {
return (
<div style={{ position: "relative" }}>
{/* 背面に配置されるボタン(z-index: 1で下に表示) */}
<button
onClick={onBackButtonClick}
style={{
position: "absolute",
top: "10px",
left: "10px",
zIndex: 1,
}}
>
背面ボタン
</button>
{/* 前面に配置されるボタン(z-index: 2で上に表示) */}
<button
onClick={onFrontButtonClick}
style={{
position: "absolute",
top: "10px",
left: "10px",
zIndex: 2,
}}
>
前面ボタン
</button>
</div>
);
};
このコンポーネントに対して、次のように前面、背面それぞれのボタンをクリックするテストをjsdom環境で実行した場合、テストは成功します。
import { describe, test, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { OverlappingButtons } from "./OverlappingButtons";
describe("OverlappingButtons (jsdom環境)", () => {
test("前面ボタンをクリック", async () => {
// クリック時のイベントハンドラ用のモック関数を作成
const onBackButtonClick = vi.fn();
const onFrontButtonClick = vi.fn();
// まったく同じ位置に 2 つのボタンが重なって配置されているコンポーネントをレンダリング
render(
<OverlappingButtons
onBackButtonClick={onBackButtonClick}
onFrontButtonClick={onFrontButtonClick}
/>
);
// 前面ボタンをクリック
const frontButton = screen.getByRole("button", { name: "前面ボタン" });
await userEvent.click(frontButton);
// 前面ボタンのクリックイベントが呼ばれ、背面ボタンのクリックイベントは呼ばれないことを確認
expect(onFrontButtonClick).toHaveBeenCalledTimes(1);
expect(onBackButtonClick).not.toHaveBeenCalled();
});
test("背面ボタンをクリック", async () => {
const onBackButtonClick = vi.fn();
const onFrontButtonClick = vi.fn();
render(
<OverlappingButtons
onBackButtonClick={onBackButtonClick}
onFrontButtonClick={onFrontButtonClick}
/>
);
// 背面ボタンをクリック
const backButton = screen.getByRole("button", { name: "背面ボタン" });
await userEvent.click(backButton);
// 背面ボタンのクリックイベントが呼ばれ、前面ボタンのクリックイベントは呼ばれないことを確認
expect(onBackButtonClick).toHaveBeenCalledTimes(1);
expect(onFrontButtonClick).not.toHaveBeenCalled();
});
});
しかし、実際のブラウザ上でこのコンポーネントを表示した場合、背面に配置されているボタンはクリックできません。これは、jsdomが要素の重なり
したがってNode.
最近では、ブラウザ環境でUIコンポーネントをテストする機能を提供するライブラリやツールが増えてきたので、実際にいくつか見ていきましょう。
Vitest Browser Mode
ブラウザ環境でUIコンポーネントをテストするために、VitestではBrowser Modeという機能が実験的に提供されています。
Vitestの設定でプロバイダにPlaywrightかWebdriverIOを指定することで、Vitestで実行するテストがブラウザ上で実行されます。
導入方法としては、まず必要なライブラリをインストールします。
npm install -D vitest @vitest/browser
# Browser ModeでReactコンポーネントをレンダリング
npm install -D vitest-browser-react
# CIでもVitest Browser Modeを実行する場合はプロバイダもインストールする
# 今回はPlaywrightを使う
npm install -D playwright
VitestでBrowser Modeを使う方法はいくつかあり、Viteの設定やVitest用の設定ファイルで指定しても良いですが、今回はVitestのTest Projects機能を使って、次のように異なる設定でUIコンポーネントのテストを実行するようなプロジェクトを作成します。
*.node.{test,spec}.ts
であればNode.js環境でテストを実行 *.browser.{test,spec}.ts
であればブラウザ環境( jsdom
)でテストを実行
vitest.
を新しく作成し、上記の設定を追加します。
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
projects: [
{
// Node.js環境でテストを実行するプロジェクト
test: {
// プロジェクト名
name: "node",
globals: true,
environment: "jsdom",
// Node.js環境で実行するテストのファイル名が、xx.node.test.tsやxx.node.test.tsxのものを対象にする
include: ["**/*.node.{test,spec}.{ts,tsx}"],
},
},
{
// ブラウザ環境でテストを実行するプロジェクト
test: {
name: "browser",
// ブラウザ環境で実行するテストのファイル名が、xx.browser.test.tsやxx.browser.test.tsxのものを対象にする
include: ["**/*.browser.{test,spec}.{ts,tsx}"],
browser: {
enabled: true,
// 使用するブラウザの設定
instances: [{ browser: "chromium" }],
// 使用するプロパイダ
provider: "playwright",
},
},
},
],
},
});
このTest Projects機能を使うことで、指定したプロジェクトごとにテストを実行することもできます。
{
"scripts": {
// projectで定義したすべてのプロジェクトを実行
"test": "vitest",
// Node.js環境でテストを実行するプロジェクトを実行
"test:node": "vitest --project node",
// ブラウザ環境でテストを実行するプロジェクトを実行
"test:browser": "vitest --project browser"
}
}
次にブラウザ環境で実行するテストファイルForm.
)
import { describe, test, expect, vi } from "vitest";
import { render } from "vitest-browser-react"; // ①
import { page, userEvent } from "@vitest/browser/context"; // ②
import { Form } from "./Form";
describe("Form", () => {
test("フォームに名前を入力して送信ボタンをクリックすると、onSubmitが呼ばれる", async () => {
const onSubmit = vi.fn();
render(<Form onSubmit={onSubmit} />);
// 入力フィールドを取得
const nameField = page.getByRole("textbox", { name: "Name" });
// 入力フィールドにテキストを入力
await userEvent.type(nameField, "nus3");
// 送信ボタンをクリック
const submitButton = page.getByRole("button", { name: "送信" });
await userEvent.click(submitButton);
// onSubmit関数が正しい引数で呼ばれたか確認
expect(onSubmit).toHaveBeenCalledWith("nus3");
});
});
このテストファイルではVitestのBrowser ModeでReactコンポーネントをレンダリングし、実際のブラウザ上でイベントを実行するために次のライブラリを使います。
- ①
vitest-browser-react
- ②
@vitest/
browser/ context
jsdom
を使ったUIコンポーネントのテストではイベントをシミュレートするのに@testing-library/
を使いましたが、VitestのBrowser Modeでは実際にブラウザ上でイベントを実行するために、@vitest/
が提供するuserEvent
を使います。
最後にBrowser Modeでテストを実行することで、実際のブラウザ上でUIコンポーネントをテストされていることを確認できます。
npm run test:browser

Storybook
Storybookは、UIコンポーネントを開発するためのツールで、Storyと呼ばれるコンポーネントの状態を定義することで、Storybook
このStorybookではUIコンポーネントをブラウザ上でテストする、次のような機能を提供しています。
play
関数を使ったStorybook上でのテスト@storybook/
テストランナーを使ったテストtest-runner @storybook/
を使ったテストaddon-vitest
Storybookの導入方法と、Storybookが提供する機能をそれぞれ見ていきましょう。
Storybookの導入
Storybookの導入方法としては、まず必要なライブラリをインストールします。
npm install -D storybook @storybook/react-vite
次に、Storybookの設定ファイル.storybook/
を作成します。ここでは、ReactとViteを使ったプロジェクトを想定しています。
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
// srcディレクトリ以下の.stories.拡張子のファイルを対象にする
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
framework: {
// ReactとViteを使ったプロジェクト
name: "@storybook/react-vite",
options: {},
},
};
export default config;
そして、Storybook上に描画したいUIコンポーネントのStoryを作成します。今回は、先ほど作成したフォームコンポーネントを例に、Form.
を作成します。
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Form } from "./Form";
// Storybook用のメタデータの設定
const meta = {
title: "Components/Form",
// Storybook上で描画するコンポーネント
component: Form,
parameters: {
// Storybook上のコンポーネントを描画するレイアウトの設定
layout: "centered",
},
} satisfies Meta<typeof Form>;
export default meta;
type Story = StoryObj<typeof meta>;
// Story
export const Default: Story = {
// Formコンポーネントに渡すpropsをargsとして定義
args: {
onSubmit: (name) => {
console.log(`Submitted name: ${name}`);
},
},
};
最後に、Storybookを起動するためのnpm scriptsを追加し、実行します。
{
"scripts": {
"storybook": "storybook dev -p 6006"
}
}
Storybookが起動したら、ブラウザでhttp://
にアクセスすることで、Storybook上に描画されたフォームコンポーネントを確認できます。

play
関数を使ったStorybook上でのテスト
Storybook上に描画したUIコンポーネントに対してplay
関数を使うことで、定義したユーザーの動作をStorybook上で視覚的に確認できます。
今回は先ほど作成したフォームコンポーネントに対して、play
関数内で値を入力し、送信ボタンをクリックする一連の操作を定義してみましょう。
import type { Meta, StoryObj } from "@storybook/react-vite";
import { userEvent, fn, waitFor, expect } from "storybook/test";
import { Form } from "./Form";
// ...
// play関数を使ったStory
export const SubmitInteraction: Story = {
args: {
// fn()でモック関数を作成
onSubmit: fn(),
},
// canvasがStorybook上に描画されたコンポーネント
play: async ({ canvas, step, args }) => {
// 実際のユーザー操作をステップごとに定義
await step("名前フィールドに名前を入力", async () => {
await userEvent.type(
canvas.getByRole("textbox", { name: "Name" }),
"nus3"
);
});
await step("送信ボタンをクリック", async () => {
await userEvent.click(canvas.getByRole("button", { name: "送信" }));
});
await step("submit時に入力された値が渡される", async () => {
// Formコンポーネントのpropsとして渡したargs.onSubmitが呼ばれたことを確認
await waitFor(() => expect(args.onSubmit).toHaveBeenCalledWith("nus3"));
});
},
};
Storybookを起動するとInteractionsパネルが表示され、play
関数で定義したステップごとの操作をStorybook上で確認できます。

このplay
関数によって、事前にステップを定義することで、そのステップごとに視覚的にUIコンポーネントの状態を確認できます。この点が、ほかのテストフレームワークにはない特徴だと筆者は考えています。
@storybook/test-runner
を使ったテスト
@storybook/
というテストランナーを使うことで Storyのplay
関数で定義したテストを実行できます。
@storybook/
では内部でJestとPlaywrightを使い、事前に起動したStorybookに対して、play
関数のテストを実行します。
導入方法としては、必要なライブラリをインストールし、package.
にテスト用のnpm scriptsを追加します。
npm install -D @storybook/test-runner
{
"scripts": {
"test-storybook": "test-storybook"
}
}
事前にStorybookを起動し、test-storybook
を実行することでplay
関数で定義したテストを実行できます。
npm run test-storybook > storybook-test@1.0.0 test-storybook > test-storybook PASS browser: chromium src/components/Form.stories.tsx Components/Form SubmitInteraction ✓ play-test (55 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 3.89 s Ran all test suites.
@storybook/addon-vitest
を使ったテスト
@storybook/
を使うことで、Storyのplay
関数で定義したテストをVitestのBrowser Modeで実行できます。
事前にStorybookを起動する必要がない点と、内部ではJestではなくVitestが使われる点が@storybook/
との大きな違いです。
導入方法としては、まず必要なライブラリをインストールします。
npm install -D @storybook/addon-vitest
次にStorybookの設定ファイル.storybook/
に、@storybook/
を追加します。
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
// ...
addons: ["storybook/addon-vitest"],
// ...
};
export default config;
.storybook/
を作成し、Vitestのセットアップ時の処理を追加します。このセットアップ時の処理の中で、VitestにStorybookの設定情報を認識させるためのsetProjectAnnotations
を呼び出します。
import { beforeAll } from "vitest";
import { setProjectAnnotations } from "@storybook/react-vite";
const project = setProjectAnnotations([]);
beforeAll(project.beforeAll);
VitestのTest Projects機能を使い、vitest.
にStorybook用のプロジェクトの設定を作成します。
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
// __dirnameがない環境でも動作するように、Node.jsのモジュールを使ってdirnameを取得
const dirname =
typeof __dirname !== "undefined"
? __dirname
: path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
// @storybook/addon-vitestをプラグインとして追加
plugins: [
storybookTest({
// .storybookディレクトリのパスを指定
configDir: path.join(dirname, ".storybook"),
// Storybookを起動するためのnpm scriptを指定
storybookScript: "npm run storybook --ci",
}),
],
test: {
name: "storybook",
// VitestのBrowser Modeを有効にする
browser: {
enabled: true,
headless: true,
instances: [{ browser: "chromium" }],
provider: "playwright",
},
setupFiles: [".storybook/vitest.setup.ts"],
},
});
最後に、npm scriptsにStorybook用のテストを実行するためのスクリプトを追加します。
{
"scripts": {
"test:storybook": "vitest --project storybook"
}
}
実際にnpm run test:storybook
を実行するとStorybookを起動しなくてもテストが実行されていることが確認できます。
npm run test:storybook > storybook-test@1.0.0 test:storybook > vitest run --project storybook info Using tsconfig paths for react-docgen RUN v3.1.3 ✓ storybook (chromium) src/components/Form.stories.tsx (2 tests) 141ms ✓ Submit Interaction 80ms Test Files 1 passed (1) Tests 2 passed (2) Start at 18:30:07 Duration 1.79s
そのほか、ブラウザ環境でUIコンポーネントをテストする機能を提供しているライブラリやツール
ブラウザ環境でUIコンポーネントをテストする機能を提供しているテストフレームワークとして、VitestのBrowser ModeとStorybookを中心に紹介しました。このほかにも次のように、同様の機能を提供しているテストフレームワークはいくつかあります。
最後に
本記事ではフロントエンドにおけるテストフレームワークの特徴とUIコンポーネントに焦点を当てたテスト手法を紹介しました。今回は紹介しきれませんでしたが、このほかにも見た目の変化があるかを確認するVisual Regression Testingやアクセシビリティのテストなど、Webフロントエンドにおけるテスト手法やツールはまだまだあります。
冒頭でも触れたように、Webフロントエンドの変化に伴い、取れうるテストの選択肢も増えてきました。選択肢が増えることによって、その手法やツールを学ぶことがたいへんになるかもしれませんが、選択肢が多いということは今までよりも自社のプロダクトやプロジェクトに適したテスト手法を選べるということでもあります。
本記事がモダンなフロントエンドにおけるテストを考えるきっかけになればうれしいです。
リポジトリ
本記事で紹介した内容は次のリポジトリにまとめていますので、利用しているライブラリのバージョンや設定を確認したい場合は、こちらを参考にしてください。