Claude Media
Claude CodeでFakerのテストデータを作る — 本番データは読ませずDDLだけ渡す

Claude CodeでFakerのテストデータを作る — 本番データは読ませずDDLだけ渡す

本番のダンプは渡さず、DDLだけを入力にFakerでseed固定のテストデータを作る手順。Read denyで読ませない設定、外部キー整合の検証、再現性の落とし穴まで。

Claude Codeにテストデータを作らせるとき、一番安全な入力は本番データではなくスキーマ定義(DDL)です。DDLだけを渡し、Fakerのseedを固定したスクリプトを書かせれば、個人情報を含まず、何度実行しても同じ内容になる、外部キー整合の取れたデータが手に入ります。

本番のダンプやCSVを「参考に」読ませる方法は手軽ですが、実在の氏名やメールアドレスがそのままセッションに乗ります。ここでは入力を絞り、さらに読ませない設定まで入れる手順を、手元で実行した出力つきで示します。

なぜ本番データではなくDDLだけを渡すのか

テストデータに必要な情報は、列名・型・NOT NULL・UNIQUE・CHECK・外部キーといった「形」です。値そのものは要りません。DDLにはこの形がすべて書かれています。

本番データを読ませると、次の問題が出ます。

  • 実在の個人情報がプロンプトとツール出力に入る
  • 生成物が本番の値を写して、匿名化したつもりの偽データに実データが混ざる
  • 「見た目が本物らしい」ことに頼って、制約の確認がおろそかになる

DDLなら、読ませる対象がテーブル定義の数十行に収まります。サンプル値が欲しいときも、列の意味(氏名・住所・金額)を文章で添えれば足ります。

本番データを読ませない設定を先に入れる

指示で「本番データは見ないで」と頼むだけでは足りません。権限ルールはモデルへの指示ではなく、Claude Code側が強制するものだからです。.claude/settings.jsonにReadのdenyルールを置きます。

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./secrets/**)",
      "Read(./dumps/**)",
      "Read(./*.sql.gz)"
    ]
  }
}

パターンはgitignoreの書式です。Read(.env)のように素のファイル名で書くと、カレントディレクトリ以下のどの深さの.envにも当たります。denyルールのReadは、Claude組み込みのファイル読み取りツールだけでなく、catやheadのようにClaudeが認識できるBashのファイル読み取りコマンドにも効きます。さらにEditとWriteでの同じパスへの書き込みも止めます。

ただし、効かない経路があります。grep -r pattern .のようにファイルを名指ししないコマンドや、PythonやNodeのスクリプトが自分でファイルを開く場合は、Readルールの対象外です。ここは次の節の設計に直結します。Claudeに書かせたシードスクリプトが、環境変数のDB接続先を自分で読んで本番に繋がる経路は、権限ルールでは塞げません。OSレベルで塞ぎたいときは、サンドボックスを有効にします。選び方はClaude Codeのサンドボックスの選び方にまとめています。

DDLを入力にFakerスクリプトを書かせる

Fakerはfakeデータを返すPythonライブラリで、pip install Fakerで入ります。氏名や住所などを返す生成関数が「プロバイダー」にまとまっていて、Faker('ja_JP')のようにロケールを渡すと日本語の氏名が出ます。

Claude Codeへの依頼は、入力・出力・制約を分けて書きます。

schema.sql(DDLのみ。実データは無い)を入力に、Fakerで
テストデータを生成する seed_data.py を作ってください。
 
- Faker.seed() を先頭で固定し、何度実行しても同じ結果にする
- ロケールは ja_JP
- メールアドレスは user<id>@example.com の形にする
- 日時は実行日に依存しない固定の範囲(2026-01-01〜2026-06-30)から選ぶ
- 子テーブルの外部キーは、先に作った親テーブルのidから選ぶ
- PRAGMA foreign_keys = ON のSQLiteで全INSERTが通ること
- 本番DBには接続しない。接続文字列も読まない

最後の2行が肝です。接続しないことを依頼文に明記し、Claudeが実行する範囲を「ローカルの空DBに作る」に限定します。

実際に動かしたスクリプトと出力

以下は、2テーブル(usersとorders)のDDLで試した最小構成です。Python 3.12.2、Faker 40.43.0で実行しました。

import os
import sqlite3
from datetime import datetime
from faker import Faker
 
Faker.seed(20261027)
fake = Faker("ja_JP")
START, END = datetime(2026, 1, 1), datetime(2026, 6, 30)
 
DDL = """
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  created_at TEXT NOT NULL
);
CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  user_id INTEGER NOT NULL REFERENCES users(id),
  total_yen INTEGER NOT NULL CHECK (total_yen >= 0),
  ordered_at TEXT NOT NULL
);
"""
if os.path.exists("test.db"):
    os.remove("test.db")
con = sqlite3.connect("test.db")
con.execute("PRAGMA foreign_keys = ON")
con.executescript(DDL)
 
for i in range(1, 6):
    con.execute(
        "INSERT INTO users VALUES (?, ?, ?, ?)",
        (i, f"user{i}@example.com", fake.name(),
         fake.date_time_between(START, END).isoformat(timespec="seconds")),
    )
user_ids = [r[0] for r in con.execute("SELECT id FROM users")]
for i in range(1, 9):
    con.execute(
        "INSERT INTO orders VALUES (?, ?, ?, ?)",
        (i, fake.random_element(user_ids),
         fake.random_int(min=500, max=30000),
         fake.date_time_between(START, END).isoformat(timespec="seconds")),
    )
con.commit()
 
for table in ("users", "orders"):
    for row in con.execute(f"SELECT * FROM {table} LIMIT 3"):
        print(row)

最後のループがusersとordersの先頭3行を出力します。実行すると、次のようになりました。氏名はFakerが組み立てた架空の値で、実在の人物のデータは一切使っていません。

(1, 'user1@example.com', '佐藤 京助', '2026-02-04T07:39:44')
(2, 'user2@example.com', '加藤 翔太', '2026-01-05T14:59:52')
(3, 'user3@example.com', '吉田 結衣', '2026-05-29T02:42:48')
(1, 4, 26043, '2026-02-28T18:36:14')
(2, 2, 9006, '2026-03-08T07:17:53')
(3, 3, 3374, '2026-01-03T01:48:08')

スクリプトは実行のたびにtest.dbを作り直します。2回続けて実行して出力をcmpで比べたところ、差分はありませんでした。ordersのuser_idはusersに実在するidだけから選んでいるので、外部キー違反は出ません。逆に、存在しない親(user_id = 999)を入れた場合は、次のエラーで止まりました。

IntegrityError: FOREIGN KEY constraint failed

この「わざと壊して落ちることを確かめる」一手は、Claudeに追加で頼んでおくと、制約が本当に効いているかの確認になります。

seed固定の落とし穴

Fakerのseedは、同じバージョンで同じメソッドを同じ順序で呼べば、同じ結果を返す仕組みです。ここに3つの注意があります。

バージョンを固定する。Fakerのドキュメントは、データセットの更新で結果がパッチバージョン間でも変わることがあるとしています。出力値をテストに直書きするなら、Faker==40.43.0のようにパッチ番号まで固定します。

インスタンスにseedを呼ばない。 fake.seed(0)はTypeErrorになります。クラスメソッドのFaker.seed(0)を使います。Claudeが古い記事の書き方をなぞってfake.seed()を書くことがあるので、レビューで見る価値のある箇所です。

生成の順序が結果を変える。seedは共有の乱数生成器を初期化します。列を1つ増やしてfake.name()の呼び出しを挟むと、以降の値がずれます。スナップショットを比較するテストでは、列追加のたびに期待値が変わると考えてください。

もう一点、日時の扱いです。date_time_this_year()のような「今年」を指す関数は、名前のとおり実行時点を基準にする指定です。依頼文で固定の開始日・終了日を渡す形にしたのは、実行日に左右されない出力にするためです。

ユニーク制約のある列には、fake.unique.first_name()のような.uniqueが使えます。ただし候補が尽きるとUniquenessExceptionで止まります。メールアドレスのように連番で作れる列は、Fakerに任せず連番で作るほうが安全です。上の例でuser{i}@example.comとしたのも同じ理由です。

検証をClaudeに回させる

生成して終わりにせず、検証コマンドを実行させて出力を読ませます。

python seed_data.py > run1.txt
python seed_data.py > run2.txt
cmp run1.txt run2.txt
sqlite3 test.db "PRAGMA foreign_key_check;"

PRAGMA foreign_key_checkが何も返さなければ、外部キー違反はありません。cmpが何も表示せずに終われば、2回の出力が一致しています。seed固定が効いている証拠です。

foreign_key_checkはファイルに残ったDBを調べるため、スクリプトは:memory:ではなくtest.dbに書いています。SQLiteの外部キー制約は接続ごとにPRAGMA foreign_keys = ONを実行して初めて有効になるので、依頼文にも同じ行を入れています。これを忘れたスクリプトは、親のいない注文を入れても黙って通ります。CLAUDE.mdには次のように書いておくと、毎回の依頼文を短くできます。

## テストデータ生成
- 入力は schema.sql(DDL)だけ。dumps/ や .env は読まない
- Faker.seed() を固定し、日時は固定範囲から選ぶ
- 生成後に foreign_key_check と2回実行の diff を必ず実行する
- Fakerのバージョンは requirements に固定する

生成物がDDLの変更に追随するかは、スキーマ側の運用次第です。マイグレーションをClaudeに書かせる流れはAlembicのマイグレーションを書く手順に、Postgresのスキーマ設計を任せる入れ方はpg-aiguideの記事にあります。DDLが更新されたら、シードスクリプトにも同じ変更を反映させる依頼を1回挟みます。

検証コマンドだけを許可して、承認の手間を減らす

シードスクリプトの実行と検証は、1回の作業で何度も回ります。毎回の承認プロンプトを避けたい場合は、実行するコマンドを絞ったallowルールを足します。

{
  "permissions": {
    "allow": [
      "Bash(python seed_data.py > run1.txt)",
      "Bash(python seed_data.py > run2.txt)",
      "Bash(cmp run1.txt run2.txt)",
      "Bash(sqlite3 test.db \"PRAGMA foreign_key_check;\")"
    ],
    "deny": [
      "Read(./.env)",
      "Read(./dumps/**)"
    ]
  }
}

ルールの評価順はdeny、ask、allowの順で、最初に当たったものが結果を決めます。そのため、allowで広めに許可しても、denyの対象は許可に化けません。逆に言えば、allowの穴をdenyで塞ぐ設計は成り立ちますが、denyの穴をallowで開けることはできません。

Bashのルールは、コマンドの文字列に対して照合します。上のルールは、検証節で流す4本のコマンドをそのまま書いた完全一致です。&&や;、|でつないだ複合コマンドは、各サブコマンドが別々にルールへ当たる必要があります。

sqlite3 test.db *のようにワイルドカードを使うと、SQL文を何でも流せる状態になります。読み取り専用の確認に限りたいので、PRAGMA foreign_key_check;まで書いた完全一致にしています。

pytestで使うときはfaker_seedフィクスチャ

Fakerはpytest用のプラグインを同梱していて、テスト関数の引数にfakerを書くとフィクスチャとして受け取れます。seedは、faker_seedというフィクスチャを自分で定義して固定します。

import pytest
 
@pytest.fixture(scope="session")
def faker_seed():
    return 20261027
 
def test_user_name_is_stable(faker):
    assert faker.name()

テストの中でその都度データを作る方式は、DBに投入するシード方式と使い分けます。単体テストの入力値にはfakerフィクスチャが向き、結合テストや画面確認に使う「ひとまとまりのデータ」にはシードスクリプトが向きます。どちらもClaudeには、上のCLAUDE.mdと同じ方針(DDLだけ・seed固定・検証付き)で頼めます。ライブラリ側のseedの仕様は共通なので、バージョンを固定する理由も変わりません。

日本向けデータでの確認ポイント

ja_JPロケールを指定すれば、氏名や住所が日本語の形式で出ます。複数ロケールを混ぜたいときは、Faker(['it_IT', 'en_US', 'ja_JP'])のようにリストを渡します(v3.0.0以降)。指定ロケールにプロバイダーが無い項目は、en_USにフォールバックします。

生成した氏名が実在の人物と偶然同じになる可能性は、否定できません。個人を特定できるデータとして扱わない設計にするなら、メールアドレスはexample.comのようなサンプル用のドメインに固定し、氏名は表示確認が目的の列に限ります。

まとめ

本番データを入力にしない設計は、権限設定・依頼文・検証の3点がそろって初めて機能します。Readのdenyで読ませず、DDLだけで依頼し、2回実行の差分と外部キー検査を回す。この3点を、まずローカルの空DBで試してください。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. faker.readthedocs.io/en/stable
この記事を共有:XはてブLinkedIn