如何用 AI 生成的用户数据填充测试数据库
硬编码的 Fixture 文件会腐化,复制生产数据又有合规风险。这里有一套基于 Schema 的方式,按需生成真实的测试用户数据——附完整可运行代码。
当你们团队第三次提交一个包含十二行 "John Doe" 和 "test@example.com" 的 users.json fixture 文件时,这个问题就会变得非常具体。测试通过了,staging 环境看起来也没问题。但当一个真实用户注册时用了阿拉伯文姓名、一个 fixture 里从未出现的角色组合,以及三个同时开启的权限标志——某些地方就崩了。
问题的根源不是那个 bug,而是你的测试数据从来就不够真实,无法暴露它。
为什么 Fixture 文件在规模变大后会失效
静态 fixture 文件有三种会随时间叠加的失效模式。
第一,它们是由最初搭建项目的人写的,反映的是那个人对"典型用户"的心理模型。如果产品已经迭代了很长时间,那个模型很可能已经失准。
第二,它们默认不覆盖边界情况。没有人会写一个 preferred_pronouns 字段为空数组的 fixture,也没有人会测试 locale 为 pt-BR 的用户在只测过 en-US 的应用里会发生什么。
第三,它们需要手动更新——也就是说,它们根本不会被更新。18个月前的 fixture 里有字段已经不存在,而这期间新增的三个字段根本就没有。
替代方案不是复制生产数据(GDPR 不允许,而且你只是在导入真实的 bug)。替代方案是生成合成数据:真实、多样,并由你实际的 Schema 驱动。
准备工作:AI Persona Generator API
AI Persona Generator 提供了一个 REST 接口 POST /api/v1/personas,接收 Schema 描述并返回结构化的用户记录。最简请求体如下:
{ "personaCount": 20, "format": "json", "projectDescription": "一个有角色权限控制的 B2B SaaS 项目管理工具", "outputLanguage": "zh-CN", "schemaMode": "hybrid", "personalPrompt": false, "customFields": true, "enableAvatar": false, "fields": [ { "id": "f1", "name": "role", "type": "Job Role", "description": "用户在组织中的角色", "values": ["admin", "editor", "viewer", "billing_manager"], "valueWeightPercent": [10, 40, 40, 10], "multi": false }, { "id": "f2", "name": "subscription_tier", "type": "Subscription Tier", "description": "账户套餐", "values": ["free", "pro", "enterprise"], "valueWeightPercent": [60, 30, 10], "multi": false } ] }
valueWeightPercent 是大多数工具忽略的部分。你不是在要随机数据,而是在要符合真实分布的数据。在大多数 SaaS 产品里,免费用户远多于企业用户。这个比例在测试分页、计费逻辑或任何按套餐收费的功能时非常重要。
发起请求
你需要在账户设置里获取 API Key。以下是一个可直接运行的 curl 请求:
curl -X POST https://aipersonagen.com/api/v1/personas \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "personaCount": 20, "format": "json", "projectDescription": "B2B SaaS 项目管理工具", "outputLanguage": "zh-CN", "schemaMode": "hybrid", "personalPrompt": false, "customFields": true, "enableAvatar": false, "fields": [...] }'
响应会返回一个 taskId。对于较大批次的生成是异步的,需要轮询 GET 接口:
curl "https://aipersonagen.com/api/v1/personas?taskId=YOUR_TASK_ID" \ -H "Authorization: Bearer YOUR_API_KEY"
当 success 为 true 时,data 字段包含生成记录的 JSON 字符串。
接入 Seed 脚本
下面是一个 Node.js seed 脚本,使用 pg 库将生成的用户插入 PostgreSQL 数据库:
const { Client } = require('pg'); async function generatePersonas(apiKey, payload) { const res = await fetch('https://aipersonagen.com/api/v1/personas', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify(payload), }); const { taskId } = await res.json(); while (true) { await new Promise(r => setTimeout(r, 2000)); const poll = await fetch( `https://aipersonagen.com/api/v1/personas?taskId=${taskId}`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const result = await poll.json(); if (result.success) return JSON.parse(result.data); } } async function seed() { const client = new Client({ connectionString: process.env.DATABASE_URL }); await client.connect(); const personas = await generatePersonas(process.env.AI_PERSONA_API_KEY, { personaCount: 50, format: 'json', projectDescription: '有角色权限控制的 B2B SaaS 项目管理工具', outputLanguage: 'zh-CN', schemaMode: 'hybrid', personalPrompt: false, customFields: true, enableAvatar: false, fields: [ { id: 'f1', name: 'role', type: 'Job Role', description: '用户在组织中的角色', values: ['admin', 'editor', 'viewer', 'billing_manager'], valueWeightPercent: [10, 40, 40, 10], multi: false }, { id: 'f2', name: 'subscription_tier', type: 'Subscription Tier', description: '账户套餐', values: ['free', 'pro', 'enterprise'], valueWeightPercent: [60, 30, 10], multi: false } ] }); for (const p of personas) { await client.query( `INSERT INTO users (full_name, email, role, subscription_tier) VALUES ($1, $2, $3, $4) ON CONFLICT (email) DO NOTHING`, [p.full_name, p.email, p.role, p.subscription_tier] ); } console.log(`已插入 ${personas.length} 个用户。`); await client.end(); } seed().catch(console.error);
将它加入 package.json 的 scripts,你就有了一个 npm run seed,每次都能生成符合 Schema 的新鲜数据。
在 Jest / Vitest 中使用
如果你更倾向于内联测试数据而非预填充数据库,同一个 API 也可以作为测试 fixture 工厂。一个简单的 helper 可以保持测试代码整洁:
// test-helpers/persona-factory.js let cachedPersonas = null; export async function getTestPersonas(count = 10) { if (cachedPersonas) return cachedPersonas.slice(0, count); // ... 同上的 fetch + poll 逻辑 cachedPersonas = result; return cachedPersonas.slice(0, count); }
在整个测试套件中缓存结果,避免每次测试都调用 API。每次 CI 运行生成一次,存到临时文件,重复使用。
什么情况下不适用
直接说:这个方案最适合集成测试和 staging 环境,而不是单元测试。如果你在测试一个格式化姓名的函数,你不需要 AI 生成的画像——几个硬编码的字符串更快更可预测。
它真正发挥价值的场景,是测试质量依赖于数据多样性的地方:权限逻辑、计费边界、国际化、搜索和过滤,或者任何用户属性的真实分布会影响 bug 是否被发现的功能。
Fixture 文件曾经有它的时代。对于超出玩具规模的任何测试,是时候改用生成的方式了。