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の原則

問題を根本から解決するため、以下の設計方針を採用しました:

  1. 設定ファイルを唯一の真実の源(Single Source of Truth)とする
  2. 設計書・実装・テストはすべて同一の情報源を参照
  3. コード内への直接的な値の埋め込みを排除

※ リテラル値 : コード内に直接書かれた値のこと(例: カテゴリ名、件数)。
※ ハードコード : 値を直接コードに書き込むこと。変更時に複数箇所を修正する必要がある。

この方針により、値の変更が設定ファイルの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.yaml 1箇所で完結

  • 以前: 4-5ファイルを同期する必要があった

  • 現在: spec.yamlを変更するだけで全体に反映

  • テストコードの意図が明確になった

  • 以前: 件数チェックで何をテストしているか不明瞭

  • 現在: 存在確認で「特定条件の記事が表示されるか」が明確

学んだこと・Tips

良かった判断

  1. 段階的実施(Phase 1-2優先)

    • テスト専用記事の作成とE2Eテスト修正を優先
    • 早期に効果を実感でき、モチベーション維持につながった
  2. テスト専用記事の作成

    • 本番データとテストデータを完全分離
    • タイトルに【テスト記事】を付けて識別しやすく
  3. 存在確認への変更

  • 件数チェック → 存在確認に変更
  • データの増減に強いテストに

注意点

  1. spec.yamlの肥大化防止

    • セクション分割(categories, test_articlesなど)
    • スコープを明確にし、入れるべきものを厳選
  2. テストの読みやすさ

  • ヘルパー関数でラップし、抽象度を適切に保つ
  • コメントで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設計 で解決しました。

重要なポイント

  1. spec.yamlを唯一の真実の源とする - 値の変更が1箇所で完結
  2. テスト専用データを分離する - 本番データへの依存を排除
  3. 存在確認でテストを書く - データ増減に強いテストに
  4. 段階的に実施する - 早期に効果を実感し、モチベーション維持

この手法は、他のプロジェクトセクションにも適用可能です。設計書・実装・テストの情報管理に課題を感じている方は、ぜひ参考にしてみてください。

SSOTの思想はテストデータだけでなく、TypeScriptの型定義にも適用できます。AIとの協調で型定義を「生きた仕様書」へと昇華させた5回のシリーズでは、基底型からの派生・責務の分離・ドメイン知識の集約という段階的なアプローチが詳述されています。また、spec.yamlをE2EテストのセレクタIDにも適用するextractTestIdパターンを使うと、UIの文言変更でテストが壊れる問題も根本から解消できます。

参考リソース

あなたのリポジトリへの適用

どちらを採用しますか?(1つ選択)

SAFETY|安全性を高める

E2EテストがSSOTとしてspec.yamlのみからデータを取得しているか確認する

調査:

  1. E2Eテストファイルで直接記事タイトル・件数・カテゴリ名をハードコードしている箇所を特定せよ
  2. spec.yaml経由でテストデータを取得しているか確認せよ
  3. 本番データの追加・変更でE2Eテストが壊れないか確認せよ

この内容をClaude Codeに貼り付けて実行します

SEPARATION|責務を分離

テストデータ定義とアプリケーションデータ定義の責務分離を確認する

調査:

  1. spec.yaml内でテスト専用フィクスチャとアプリケーション設定値が明確に分離されているか確認せよ
  2. テストが参照するspec.yamlの構造がテストの可読性を損なっていないか確認せよ
  3. 新しいE2Eテストを追加する際にspec.yamlのどこにデータを追加すべきか明確か確認せよ

この内容をClaude Codeに貼り付けて実行します

外部コードのローカル実行にはリスクがあります。ブラウザ環境での実行を推奨します。