VRTで確認すること・しないことを整理する
こんにちは 👋
最近、VRT 欲が高まっているので今回は VRT に対する個人的な考え方をまとめたいと思います!
コンポーネント修正による UI 崩れに気付きやすくなる VRT ですが、VRT はおろか Storybook も導入していないような状態だと「全部の表示パターンをテストしないとダメなのかな?」、「どの部分を VRT の対象にすべき?」と色々迷うことがあると思います。Storybook があればそれをベースに VRT を作っていけば良いのですが、Storybook を使ってコンポーネントを管理する文化のないところこそ VRT をやって UI のデグレを防ぎたいと思うもの。
今回はそんな状態でもある程度の方針を立てられるように、VRT をこれから始める人が、何をどこまでテストすべきか判断できる基準を整理したいと思います。小さく始めて、徐々に広げていくための考え方を紹介したいと思います!
VRT は「見た目の変化を検出する」テストであって「仕様テスト」ではない
まず、VRT が何を検出するのかを理解することが大切です。
VRT が検出するのはピクセルレベルの差分です。画面のスクリーンショットを比較して、「見た目が変わっていないか」をチェックします。
これは言い換えると:
VRT は「画面が壊れていないか」を見る
↓
「画面の機能が正しいか」は別のテスト(React Testing Library などを使った振る舞いテストや E2E)の責務
という役割分担になります。
スナップショットテストと混同されがちですが、スナップショットテストは DOM 構造(HTML の文字列)を比較するのに対して、VRT は実際にレンダリングされた画面(ピクセル)を比較します。つまり、VRT は CSS、フォント、画像、ブラウザの描画結果まで含めた「見た目」全体を検証するものです。
VRT で何を見るべきかの判断基準
それでは、具体的にどうやって「VRT で見るべきもの」と「VRT で見るべきでないもの」を判断すればよいのでしょうか?
自分は2 つの基準で判断するのが良いと考えています。
基準 1: 何を検証するか
VRT 向き: レイアウト構造への影響(折り返し、高さ、配置、余白)
VRT 不向き: データの正しさ(表示内容、ラベル、ステータス)
VRT はピクセル比較なので、「見た目が壊れていないか」は検出できますが、「表示内容が正しいか」は検証できません。
例えば、同じ「テキスト」でも:
- テキストの長さがレイアウトに与える影響(折り返し、高さ) → VRT で検証
- テキストの内容が正しいか(「送信」か「確認」か) → RTL など別のテストで検証
基準 2: なぜ検証するか
VRT 向き: 意図しない破壊を検出(CSS 削除ミス、マージン崩れ)
VRT 不向き: 意図的な変更(デザインリニューアル、文言修正)
VRT の真の価値は「意図しない変更」を検出することにあると思います。
例えば、共通の CSS を削除したときに影響範囲が広すぎて気づけなかった、とか、マージンの指定を間違えてレイアウトが崩れた、といった「うっかりミス」を防ぐのが VRT の役割です。
逆に、デザインリニューアルのような「意図的な変更が正しくできているか」を確認する目的で VRT を作るのは不適切だと思います。VRT は変更を検出するツールであって、変更内容の正しさを保証するものではないと思います。既存の VRT がある場合は、リニューアル期間中は差分を目視確認して baseline を更新するか、VRT をスキップする使い方が良いかなと思います。
2 つの基準の使い方
この 2 つの基準を使って判断する際は、両方の基準で VRT 向きと判断できる場合のみ、VRT の対象にするのが小さく始めていくには良いと思います。
表にまとめると以下のイメージです!
| 判断基準 | VRT 向き | VRT 不向き |
|---|---|---|
| 何を検証するか | レイアウト構造への影響 | 表示内容の正しさ |
| なぜ検証するか | 意図しない破壊を検出 | 意図的な変更を検証 |
具体例で理解する
それでは、実際のコンポーネントを例に、2 つの基準を使った判断を見ていきたいと思います!
例 1: ボタンコンポーネント(シンプルな例)
まずはシンプルなボタンコンポーネントから見ていきます。

見た目が違う 3 種類のボタン
実装コード
type ButtonProps = {
variant: "primary" | "secondary" | "danger";
children: ReactNode;
icon?: ReactNode;
};
const Button = ({ variant, children, icon }: ButtonProps) => {
const baseStyle =
"px-4 py-2 rounded font-medium flex items-center gap-2 hover:cursor-pointer";
const variantStyles = {
primary: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
danger: "bg-red-600 text-white hover:bg-red-700",
};
return (
<button type="button" className={`${baseStyle} ${variantStyles[variant]}`}>
{icon && <span className="w-5 h-5 inline-flex items-center">{icon}</span>}
{children}
</button>
);
};
このボタンコンポーネントには、primary/secondary/dangerの 3 つの variant があります。また、アイコン付き/なしのパターンもあります。
2 つの基準で判断してみる
| 対象 | 基準 1: 何を検証するか | 基準 2: なぜ検証するか | 結論 |
|---|---|---|---|
| ボタンのレイアウト | レイアウト構造への影響 | 意図しない変更を検知したい | ✅ VRT 向き |
| ボタンのラベル | データの正しさ | テキスト内容の正しさは対象外 | ❌ VRT 不向き |
| アイコン位置 | レイアウト構造への影響 | 意図しない変更を検知したい | ✅ VRT 向き |
VRT した方が良いもの
-
variant:
primary/secondary/dangerのうち 1 つ(variant による違いは色だけなのでどれか 1 つで十分) - アイコンの有無: アイコン付き/なしを各 1 つずつ(レイアウトが異なる)
- 合計: 1×2 = 2 パターン
これで、ボタンのレイアウトが崩れていないか、アイコンの位置がずれていないかを確認できます。variant による違いは色くらいなので、全パターンを VRT の対象にする意味は薄いと思います。
特にボタンの色って結構意識しないと変更できないので、それよりかは padding やボタンのサイズ自体などを調整したときにアイコンとラベルのラインがズレるとかのほうが起こりがちなので、そういうケースに気付けるようにしたいです。
VRT から除外しても良いもの
- 全バリエーションの網羅: 同じレイアウトなら代表例だけで十分
- ラベルテキストの変更検知: 文言変更は意図的なので、VRT で検出する必要なし
例 2: テーブルコンポーネント(複雑な例)
次に、もう少し複雑なテーブルコンポーネントを見ていきましょう。

テーブル
実装コード
type Status = "pending" | "completed" | "error";
type TableRow = {
id: string;
name: string;
status: Status;
description: string;
};
type TableProps = {
data: TableRow[];
};
function StatusBadge({ status }: { status: Status }) {
const statusConfig = {
pending: {
label: "保留中",
color: "bg-gray-100 text-gray-800",
icon: null,
},
completed: {
label: "完了",
color: "bg-green-100 text-green-800",
icon: <CheckIcon className="w-4 h-4" />,
},
error: {
label: "エラー",
color: "bg-red-100 text-red-800",
icon: <XIcon className="w-4 h-4" />,
},
};
const { label, color, icon } = statusConfig[status];
return (
<span
className={`w-[100px] px-2 py-1 rounded text-sm font-medium flex items-center gap-1 ${color}`}
>
{icon}
{label}
</span>
);
}
export const Table = ({ data }: TableProps) => {
return (
<table className="max-w-[800px] divide-y divide-gray-200 border border-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
名前
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
ステータス
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
説明
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{data.map((row) => (
<tr key={row.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{row.name}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<StatusBadge status={row.status} />
</td>
<td className="px-6 py-4 text-sm text-gray-500">
{row.description}
</td>
</tr>
))}
</tbody>
</table>
);
};
このテーブルの特徴としては
- ステータス列にはそれぞれの状態を表すラベルがある
- ラベルにはアイコン付き/なしのパターンがある
- 説明列はテキストが多い場合に改行される
各カラムごとに複数の表示パターンがあるというのはテーブルのあるあるだと思うのですが、この場合 VRT ではどんなケースを見れば良いのでしょうか?
2 つの基準で判断してみる
| 対象 | 基準 1: 何を検証するか | 基準 2: なぜ検証するか | 結論 |
|---|---|---|---|
| テーブルのレイアウト | レイアウト構造への影響 | 意図しない変更を検知したい | ✅ VRT 向き |
| ステータス全種類 | データの正しさ | テキスト内容の正しさは対象外 | ❌ VRT 不向き |
| アイコン付き/なしの構造 | レイアウト構造への影響 | 意図しない変更を検知したい | ✅ VRT 向き |
| 長文/短文の高さ | レイアウト構造への影響 | 意図しない変更を検知したい | ✅ VRT 向き |
VRT した方が良いもの
レイアウトが異なる代表例のみを VRT の対象にします:
-
アイコン付きステータス:
完了またはエラーのどちらか 1 つ -
アイコンなしステータス:
保留 - 長文/短文パターン: 説明文が長い場合と短い場合
これで、テーブルのレイアウトが崩れていないか、アイコンの配置が正しいか、長文によるセルの高さの変化に対してレイアウト崩れがないかを確認できます。
VRT から除外しても良いもの
- 全 3 種類のステータス網羅: レイアウトが同じなら代表例だけで十分
- ステータスの色違いだけの差分: 色の微調整は意図的な変更
- データ内容の変化: API レスポンスが変わっても、レイアウトが同じなら問題なし
代表ケースの選び方
ステータスの代表ケースを選ぶときは、見た目の「レイアウト」が異なるものを選ぶのがポイントです。
- 色だけが違うものは 1 つで代表
- アイコンの有無など、レイアウトが変わるものを優先
ロジックテストとの棲み分け
ここまで読んで、「じゃあステータスの種類が正しく切り替わること自体は、どこで確認すればいいんだろう?」と思われた方もいるかもしれません。
その確認方法はいくつか考えられますが、ひとつのやり方として、React Testing Library などのテストライブラリでコンポーネント単位のロジックをテストする方法があります。
ロジックと見た目の責務分離
- ロジックテスト: テーブルに渡されるデータに対応したステータスの切り替えが期待通りか
- VRT: ステータス表示のレイアウトが崩れないか(代表例だけ押さえる)
例えば、RTL では以下のようなテストが書けます:
test('ステータスが"pending"の場合、"保留中"と表示される', () => {
render(<StatusBadge status="pending" />);
expect(screen.getByText('保留中')).toBeInTheDocument();
});
test('ステータスが"completed"の場合、"完了"と表示される', () => {
render(<StatusBadge status="completed" />);
expect(screen.getByText('完了')).toBeInTheDocument();
});
test('ステータスが"error"の場合、"エラー"と表示される', () => {
render(<StatusBadge status="error" />);
expect(screen.getByText('エラー')).toBeInTheDocument();
});
このように、ロジック部分はテストライブラリで細かく確認しつつ、見た目の崩れは VRT で代表的なパターンだけ押さえる、という方針でテストを作成していくのが良いアプローチかなと思います。
判断基準のまとめ
最後に、これまで話してきた VRT した方が良いもの・除外しても良いものをまとめたいと思います。
VRT した方が良いもの
基準 1: レイアウト構造への影響がある
- 折り返しや ellipsis が発生する
- 長文によって高さが変わる可能性がある
- アイコンの有無でレイアウトが変わる
- レイアウトが複雑(余白、配置が崩れやすい)
基準 2: 意図しない破壊を検出したい
- レイアウトが安定している(完成度が高い)
- 共通コンポーネントなど、利用頻度が高い
VRT から除外しても良いもの
基準 1: データの正しさの検証
- 文言の正しさ
- ステータスの種類の網羅
- API レスポンスの内容
基準 2: 意図的な変更
- デザインリニューアル中
- 新規ページで UI が固まっていない
- 文言が頻繁に変わる箇所
- 色だけが違うバリエーション
まとめ
以上、VRT で確認すること・しないことについてまとめてみました。
VRT は UI 崩れという見た目でしか確認できない部分を自動で検出してくれる便利なものですが、些細なことで壊れやすく、過剰にテストケースを作成するとメンテも運用も辛くなります。なので、今回紹介した「2 つの基準」(何を検証するか・なぜ検証するか)を使って判断すれば良い塩梅で VRT 範囲を設定できると思います。
大切なのは、VRT を「見た目が壊れていないか」をチェックするツールとして使うことです。ロジックは RTL 等のテストライブラリで、見た目は VRT で、という責務分離を意識すると、テストの充実度や保守性の向上に繋がると思います!
それでは、最後まで読んでいただきありがとうございました!👋
Discussion