通过让模型读取migration SQL并生成包含空值、边界值和孤立外键的测试数据,发现传统手写fixture无法覆盖的约束冲突。作者提供了可直接复用的脚本,读取schema.sql调用免费模型端点生成测试行。
我的迁移测试又通过了。二十分钟后,生产环境在外键约束上失败了。
上周我推送了一个小的 schema 变更。测试套件说一切正常。部署结果却截然不同。
为什么我的测试会欺骗我?因为我手动编写的种子行,恰好就是我的迁移所期望的那几行。
问题不在于迁移代码本身有错。而是我的测试 fixture 只覆盖了快乐路径。
我写了五行数据,外键有效、时间戳非空、邮箱字符串规范。没有一条数据挑战新的约束。
你写 fixture 的时候会记得每一个列吗?我也不会。
一个好的 fixture 集应该包含:
每个可空列至少有一行为 null。
每个需要文本的列至少有一行为空字符串。
外键指向可能不存在的行。
值要落在列类型的边界上,而不只是中间地带。
这些我可以从一个免费模型获取。我读取迁移 SQL,发送到免费端点,请求一个 JSON 数组的行,然后把它们插入到一个临时的 SQLite 数据库中。
下面是我使用的脚本。它读取 schema.sql 文件,调用免费模型端点,返回我可以插入到 SQLite 的行。
import argparse, json, os, sqlite3, urllib.request
def read_schema(path):
return open(path, encoding='utf-8').read()
def build_prompt(schema):
return f'''You are a test-data generator.
Given this SQL schema:
{schema}
Return only a JSON array of row objects.
Each object must use column names as keys and valid SQL values.
Generate 6 rows that include nulls, empty strings, and boundary values where sensible.
Do not wrap the JSON in markdown.'''
def call_model(endpoint, key, model, prompt):
payload = {
'model': model,
'messages': [
{'role': 'system', 'content': 'You return only valid JSON.'},
{'role': 'user', 'content': prompt},
],
'temperature': 0.4,
}
req = urllib.request.Request(
endpoint,
data=json.dumps(payload).encode('utf-8'),
headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {key}'},
)
with urllib.request.urlopen(req, timeout=90) as r:
return json.load(r)
def insert_rows(db_path, table_name, rows):
con = sqlite3.connect(db_path)
cur = con.cursor()
for row in rows:
columns = ', '.join(row.keys())
placeholders = ', '.join('?' for _ in row)
cur.execute(
f'INSERT INTO {table_name} ({columns}) VALUES ({placeholders})',
list(row.values()),
)
con.commit()
con.close()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--schema', default='schema.sql')
parser.add_argument('--table', required=True)
parser.add_argument('--db', default='test.db')
args = parser.parse_args()
schema = read_schema(args.schema)
prompt = build_prompt(schema)
response = call_model(
os.environ['MONKEYCODE_ENDPOINT'],
os.environ.get('MONKEYCODE_API_KEY', ''),
os.environ.get('MODEL_NAME', 'free-current'),
prompt,
)
rows = json.loads(response['choices'][0]['message']['content'])
insert_rows(args.db, args.table, rows)
print(f'inserted {len(rows)} rows')
我不会在代码中固定模型名称。免费端点会轮转,我不希望本地工具在目录变化时坏掉。
一个硬规则:只请求 JSON。模型喜欢在数组上方或下方加一段解释。这会把解析器弄坏。
我还把 temperature 设为 0.4。更高的值会产生更多样性,但也会产生更多伪造的列名。更低的值会变得无聊,错过边界情况。
我使用 MonkeyCode 的免费模型访问和免费服务器选项来运行这个脚本,而不需要保持一个付费服务开放。
披露:本文是 MonkeyCode 产品推广的一部分。
因为端点是免费的,我不需要为这个一次性的任务配置 GPU 或管理 API 层级。我也不需要结果即时返回;等咖啡的几秒钟内完成调用就够了。
上周我给 users 表添加了一个 CHECK (age >= 0) 约束。我手写的行都是正整数,所以检查通过了。
模型生成了一行 age = -1 的数据。插入在本地失败了。这告诉我两件事:约束存在,而且我的应用代码没有处理负年龄的错误路径。
我在提交前修复了应用处理。失败留在了我的笔记本上。
模型为一个类型为 TEXT 的列生成了字符串格式的 datetime。我的 schema 检查器在插入前捕获了它,因为我做了列类型的白名单。
免费层的速率限制意味着我一次大约能生成 30 行,然后模型开始重复自己。较大的表我会分批请求。
模型无法看到跨多表的外键关系,除非我包含所有相关的 DDL。我把生成器限制为一次一张表,然后手动修复跨表行。
你需要真实的个人数据来做演示。模型凭空编造的名字和邮箱不是隐私边界。
你需要 100,000 行来浸泡测试查询计划。免费模型不会产生这种量。
你有触发器、检查约束或生成列,其语义是模型仅从 DDL 无法推断的。
不要从记忆中编写测试行。让模型读取 DDL 并提出你永远不会想到包含的行。
失败留在你的笔记本上,而不是生产环境。
如果你已经有了一个免费端点,下次做迁移时试试它,在你写下一行之前。