E2Eテストが壊れ続けた理由と、テストデータ一元管理による解決

はじめに
E2Eテストが記事追加のたびに壊れる場合、テストが本番データの件数やカテゴリ名に依存しています。
spec.yaml をSSOTにして、テストをライブデータから切り離すことで解決できます。
記事を一つ追加するたびに、テストが壊れていませんか?
- 新しい記事を公開しただけなのに、関係のないE2Eテストが失敗してデプロイが止まる。
- テストコードは正しいのに、本番データの件数に依存しているためメンテナンスが追いつかない。
- カテゴリ名が複数箇所に散らばっており、一箇所の変更のために全ファイルを検索して修正している。
この記事をお勧めしない人
- テストが頻繁に壊れても、その都度手動で修正すれば問題ないと考えている人。
- 設定ファイルとコードを同期させる「Single Source of Truth」の設計思想に興味がない人。
- 開発効率よりも、とりあえず今動くコードを書くことだけを最優先したい人。
もし一つでも当てはまらないなら、読み進める価値があるかもしれません。
テストが本番データに依存したままだと
- 記事を追加するたびに「テスト修正 → コミット → デプロイ確認」のサイクルが発生し、本来の開発が止まる。
- テストが壊れても「また記事追加のせいか」と確認を省略するようになり、本物のバグを見逃すリスクが上がる。
- カテゴリ名の変更が設計書・実装・テストの3箇所に影響し、どこかで必ず修正漏れが出る。
Single Source of Truthという明るい未来
- この記事を読めば、spec.yaml を情報の中心に据え、テストと実装を完全に同期させるSSOT設計の手法が手に入る。
- 具体的には、本番データからテストを独立させ、YAML一つで全体を制御する設計図を手に入れられる。
- この方法は、本ブログの開発において、記事追加時のテスト失敗を解消するために実証済みである。
私も同じでした
このブログのE2Eテストが「記事が N 件存在する」「カテゴリ○○がある」という条件で書かれていたため、記事を追加するたびにテストが壊れていました。
spec.yaml でカテゴリと期待値を定義し、テストをライブデータから独立させることで解決しました。記事追加後にテストを修正する作業がゼロになりました。
問題の発生:情報散在がもたらした課題
情報散在の構造的問題
プロジェクト初期、カテゴリ名という単純な情報が、設計書・実装・テストコードの複数箇所に散在していました。この状態では、値を変更する際に複数ファイルの同期が必要となり、更新漏れが頻繁に発生しました。
結果として:
- テストが原因不明で失敗する
- ドキュメントが陳腐化する
- 調査に30分以上かかることも
E2Eテストデータの依存問題
特に深刻だったのが、E2Eテストが本番データに依存していたことです。テストコードが本番記事の件数を前提としていたため、以下の問題が発生していました:
- 新記事を追加 → 記事件数が変わり、テスト失敗
- 既存記事のカテゴリ変更 → フィルタ結果が変わり、テスト失敗
- 問題特定に時間がかかる → テストコードは正しいが、データが原因
記事を追加するたびにテストを修正するという、本末転倒な状況に陥っていました。
設計方針の策定
Single Source of Truthの原則
問題を根本から解決するため、以下の設計方針を採用しました:
- 設定ファイルを唯一の真実の源(Single Source of Truth)とする
- 設計書・実装・テストはすべて同一の情報源を参照
- コード内への直接的な値の埋め込みを排除
※ リテラル値 : コード内に直接書かれた値のこと(例: カテゴリ名、件数)。
※ ハードコード : 値を直接コードに書き込むこと。変更時に複数箇所を修正する必要がある。
この方針により、値の変更が設定ファイルの1箇所で完結するようになります。
スコープの見極め
設定ファイルに何を入れ、何を入れないかの判断基準を明確にしました:
設定ファイルに入れるべき情報 :
- マスタデータ(カテゴリ一覧、タグ一覧など)
- UI要素の識別子定義
- テスト専用データ定義
- ビジネスルール値
設定ファイルに入れない情報 :
- 実装ロジックに依存する計算値
- 一時的なモックデータ
- 頻繁に変わる設定値
※ マスタデータ : システム全体で共通して使う基礎的なデータのこと(例: カテゴリ一覧、タグ一覧)。
重要なのは、 複数箇所で参照される不変の情報 を設定ファイルに集約することです。
この設計方針により、値変更時の修正ファイル数を 80%削減 し、記事追加時のE2Eテスト失敗を 完全に解消 しました。テストデータ起因の調査時間も30分/回から0分へと改善されています。
実装手順
ここからは、実際に私が実装した具体的な手順を公開します。
spec.yamlの構造定義、テスト専用記事の作成方法、E2Eテストコードのリファクタリング、そして再利用可能なヘルパー関数の実装まで、ClaudeMixで実際に記述したコードと設定を、ステップバイステップで解説します。
リファクタリングは段階的に実施し、早期に効果を実感できるようにしました。
Phase 1: テスト専用記事の作成
まず、E2Eテストで使用するテスト専用の記事を作成しました:
<!-- content/blog/posts/test-article-category-1.md -->
---
slug: "test-article-category-1"
title: "【テスト記事】カテゴリ1: 初心者向けガイド"
publishedAt: "2025-12-01"
category: "Claude Best Practices"
tags: ["guide", "beginner"]
description: "E2Eテスト専用記事: カテゴリ1とタグ2つの組み合わせテスト用"
---
E2Eテスト専用の記事です。そして、spec.yamlでテストデータを定義:
# develop/blog/posts/spec.yaml
test_articles:
- slug: "test-article-category-1"
title: "【テスト記事】カテゴリ1: 初心者向けガイド"
category_id: 1
tags: ["guide", "beginner"]
description: "E2Eテスト専用記事: カテゴリ1とタグ2つの組み合わせテスト用"
- slug: "test-article-unique-tag"
title: "【テスト記事】ユニークタグ記事"
category_id: 3
tags: ["e2e-test-only"]
description: "E2Eテスト専用記事: ユニークタグによる絞り込みテスト用"これにより、本番データとテストデータを完全に分離できました。
Phase 2: E2Eテストのリファクタリング
テストコードを、spec.yamlを参照する形に修正しました:
// tests/e2e/blog/common.spec.ts
// 修正後: spec.yaml参照、存在確認
it('should filter posts by category', async ({ page }) => {
// spec.yamlからテストデータを読み込む
const spec = await loadSpec('blog','posts');
const categoryToTest = spec.categories[2]; // カテゴリ3
const testArticles = await getTestArticlesByCategory(categoryToTest.id);
// テスト用記事が存在することを確認
expect(testArticles.length).toBeGreaterThan(0);
const testArticle = testArticles[0];
await page.goto(TARGET_URL);
// フィルタを適用
const filterToggleButton = page.getByTestId('filter-toggle-button');
await filterToggleButton.click();
const categorySelector = page.getByTestId('category-selector');
await categorySelector.selectOption(categoryToTest.name);
const filterSubmitButton = page.getByTestId('filter-submit-button');
await filterSubmitButton.click({ force: true });
// テスト用記事が表示されることを確認(件数ではなく存在確認)
const testArticleCard = page.locator(
`[data-testid="post-card"][data-slug="${testArticle.slug}"]`
);
await expect(testArticleCard).toBeVisible();
});重要な変更点 :
- 件数チェック(
toHaveCount(3))→ 存在確認 (toBeVisible())に変更 - ハードコードされた値 →
spec.yamlから取得 - 本番データ依存 → テスト専用記事を使用
この変更により、記事を追加してもテストが壊れなくなりました。
Phase 3: ヘルパー関数(※)の整備
※ ヘルパー関数 : よく使う処理をまとめた補助的な関数のこと。コードの重複を減らし、読みやすくする。
テストコードの可読性を向上させるため、ヘルパー関数を作成しました:
// tests/e2e/utils/loadSpec.ts
import yaml from 'yaml';
import fs from 'fs/promises';
export async function loadSpec('blog','posts') {
const content = await fs.readFile(
'develop/blog/posts/spec.yaml',
'utf-8'
);
return yaml.parse(content);
}
export async function getTestArticlesByCategory(categoryId: number) {
const spec = await loadSpec('blog','posts');
return spec.test_articles.filter(
(article) => article.category_id === categoryId
);
}
export async function getTestArticlesByTag(tag: string) {
const spec = await loadSpec('blog','posts');
return spec.test_articles.filter(
(article) => article.tags?.includes(tag)
);
}これにより、テストコードの意図が明確になり、保守性が大幅に向上しました。
成果と効果測定
定量的成果
リファクタリングの効果を数値で測定しました:
| 指標 | Before | After | 改善率 |
|---|---|---|---|
| 値変更時の修正ファイル数 | 4-5ファイル | 1ファイル | 80%削減 |
| E2Eテスト失敗頻度(記事追加時) | 100% | 0% | 100%改善 |
| テストデータ起因の調査時間 | 30分/回 | 0分 | 完全解消 |
定性的成果
数値では測れない、開発体験の改善も大きな成果でした:
記事追加・変更でテストが壊れなくなった
以前: 記事追加のたびにテスト修正が必要
現在: 本番記事を自由に追加・変更可能
カテゴリ名変更が
spec.yaml1箇所で完結以前: 4-5ファイルを同期する必要があった
現在:
spec.yamlを変更するだけで全体に反映テストコードの意図が明確になった
以前: 件数チェックで何をテストしているか不明瞭
現在: 存在確認で「特定条件の記事が表示されるか」が明確
学んだこと・Tips
良かった判断
段階的実施(Phase 1-2優先)
- テスト専用記事の作成とE2Eテスト修正を優先
- 早期に効果を実感でき、モチベーション維持につながった
テスト専用記事の作成
- 本番データとテストデータを完全分離
- タイトルに
【テスト記事】を付けて識別しやすく
存在確認への変更
- 件数チェック → 存在確認に変更
- データの増減に強いテストに
注意点
spec.yamlの肥大化防止- セクション分割(
categories,test_articlesなど) - スコープを明確にし、入れるべきものを厳選
- セクション分割(
テストの読みやすさ
- ヘルパー関数でラップし、抽象度を適切に保つ
- コメントで
spec.yaml参照を明記
再利用可能なパターン
このリファクタリングで得られたパターンは、他のセクションにも適用可能です:
// 汎用的なspec読み込みヘルパー
export async function loadSpec<T>(specPath: string): Promise<T> {
const content = await fs.readFile(specPath, 'utf-8');
return yaml.parse(content);
}
// 使用例
const blogSpec = await loadSpec<BlogSpec>('develop/blog/posts/spec.yaml');
const flowSpec = await loadSpec<FlowSpec>('develop/service-name/spec.yaml');まとめ
情報散在とE2Eテストデータ問題を、spec.yaml中心の Single Source of Truth設計 で解決しました。
重要なポイント
spec.yamlを唯一の真実の源とする - 値の変更が1箇所で完結- テスト専用データを分離する - 本番データへの依存を排除
- 存在確認でテストを書く - データ増減に強いテストに
- 段階的に実施する - 早期に効果を実感し、モチベーション維持
この手法は、他のプロジェクトセクションにも適用可能です。設計書・実装・テストの情報管理に課題を感じている方は、ぜひ参考にしてみてください。
SSOTの思想はテストデータだけでなく、TypeScriptの型定義にも適用できます。AIとの協調で型定義を「生きた仕様書」へと昇華させた5回のシリーズでは、基底型からの派生・責務の分離・ドメイン知識の集約という段階的なアプローチが詳述されています。また、spec.yamlをE2EテストのセレクタIDにも適用するextractTestIdパターンを使うと、UIの文言変更でテストが壊れる問題も根本から解消できます。
参考リソース
- REFACTORING_PLAN.md - 技術的実装計画の詳細
- develop/blog/posts/spec.yaml - 実際のspec.yaml
- tests/e2e/blog/common.spec.ts - リファクタリング後のE2Eテスト
