pgsql-test-seeding
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSeeding Test Databases with pgsql-test
使用 pgsql-test 为测试数据库植入数据
Load test data efficiently using loadJson, loadSql, and loadCsv methods. Create maintainable, realistic test fixtures.
借助 loadJson、loadSql 和 loadCsv 方法高效加载测试数据,创建可维护、贴近真实场景的测试夹具。
When to Apply
适用场景
Use this skill when:
- Setting up test data for database tests
- Loading fixtures from JSON, SQL, or CSV files
- Seeding data that respects or bypasses RLS
- Creating per-test or shared test data
出现以下需求时可使用该技能:
- 为数据库测试准备测试数据
- 从 JSON、SQL 或 CSV 文件加载测试夹具
- 植入需要遵循或绕过 RLS 的数据
- 创建单测试专用或多测试共享的测试数据
Seeding Methods Overview
植入方法概览
| Method | Best For | RLS Behavior |
|---|---|---|
| Inline data, small datasets | Respects RLS (use |
| Complex data, version-controlled fixtures | Respects RLS (use |
| Large datasets, spreadsheet exports | Bypasses RLS (uses COPY) |
| 方法 | 适用场景 | RLS 行为 |
|---|---|---|
| 行内数据、小型数据集 | 遵循 RLS(使用 |
| 复杂数据、版本管控的测试夹具 | 遵循 RLS(使用 |
| 大型数据集、电子表格导出数据 | 绕过 RLS(使用 COPY 命令) |
Seeding with loadJson()
使用 loadJson() 植入数据
Best for inline test data. Clean, readable, and type-safe.
typescript
import { getConnections, PgTestClient } from 'pgsql-test';
let pg: PgTestClient;
let db: PgTestClient;
let teardown: () => Promise<void>;
beforeAll(async () => {
({ pg, db, teardown } = await getConnections());
// Seed using superuser to bypass RLS
await pg.loadJson({
'app.users': [
{
id: '550e8400-e29b-41d4-a716-446655440001',
email: 'alice@example.com',
name: 'Alice'
},
{
id: '550e8400-e29b-41d4-a716-446655440002',
email: 'bob@example.com',
name: 'Bob'
}
],
'app.posts': [
{
id: 'post-1',
title: 'First Post',
owner_id: '550e8400-e29b-41d4-a716-446655440001'
},
{
id: 'post-2',
title: 'Second Post',
owner_id: '550e8400-e29b-41d4-a716-446655440002'
}
]
});
});
afterAll(async () => {
await teardown();
});Key features:
- Schema-qualified table names:
'app.users' - Explicit UUIDs for referential integrity
- Multiple tables in one call
- Order matters for foreign keys
最适合行内测试数据,代码简洁、可读性高且类型安全。
typescript
import { getConnections, PgTestClient } from 'pgsql-test';
let pg: PgTestClient;
let db: PgTestClient;
let teardown: () => Promise<void>;
beforeAll(async () => {
({ pg, db, teardown } = await getConnections());
// 使用超级用户账号植入数据以绕过 RLS
await pg.loadJson({
'app.users': [
{
id: '550e8400-e29b-41d4-a716-446655440001',
email: 'alice@example.com',
name: 'Alice'
},
{
id: '550e8400-e29b-41d4-a716-446655440002',
email: 'bob@example.com',
name: 'Bob'
}
],
'app.posts': [
{
id: 'post-1',
title: 'First Post',
owner_id: '550e8400-e29b-41d4-a716-446655440001'
},
{
id: 'post-2',
title: 'Second Post',
owner_id: '550e8400-e29b-41d4-a716-446655440002'
}
]
});
});
afterAll(async () => {
await teardown();
});核心特性:
- 带 schema 限定的表名:
'app.users' - 显式 UUID 保证参照完整性
- 单次调用可植入多张表数据
- 数据顺序对有外键关联的表有影响
Seeding with loadSql()
使用 loadSql() 植入数据
Best for complex data or version-controlled fixtures.
Create :
__tests__/fixtures/seed.sqlsql
-- Insert users
INSERT INTO app.users (id, email, name) VALUES
('550e8400-e29b-41d4-a716-446655440001', 'alice@example.com', 'Alice'),
('550e8400-e29b-41d4-a716-446655440002', 'bob@example.com', 'Bob'),
('550e8400-e29b-41d4-a716-446655440003', 'charlie@example.com', 'Charlie');
-- Insert posts with foreign key references
INSERT INTO app.posts (id, title, owner_id) VALUES
('post-1', 'Alice Post 1', '550e8400-e29b-41d4-a716-446655440001'),
('post-2', 'Alice Post 2', '550e8400-e29b-41d4-a716-446655440001'),
('post-3', 'Bob Post', '550e8400-e29b-41d4-a716-446655440002');Load in tests:
typescript
import path from 'path';
beforeAll(async () => {
({ pg, db, teardown } = await getConnections());
await pg.loadSql([
path.join(__dirname, 'fixtures/seed.sql')
]);
});Multiple SQL files:
typescript
await pg.loadSql([
path.join(__dirname, 'fixtures/users.sql'),
path.join(__dirname, 'fixtures/posts.sql'),
path.join(__dirname, 'fixtures/comments.sql')
]);Files execute in order, so put parent tables first.
最适合复杂数据或版本管控的测试夹具。
创建 :
__tests__/fixtures/seed.sqlsql
-- 插入用户数据
INSERT INTO app.users (id, email, name) VALUES
('550e8400-e29b-41d4-a716-446655440001', 'alice@example.com', 'Alice'),
('550e8400-e29b-41d4-a716-446655440002', 'bob@example.com', 'Bob'),
('550e8400-e29b-41d4-a716-446655440003', 'charlie@example.com', 'Charlie');
-- 插入带外键关联的帖子数据
INSERT INTO app.posts (id, title, owner_id) VALUES
('post-1', 'Alice Post 1', '550e8400-e29b-41d4-a716-446655440001'),
('post-2', 'Alice Post 2', '550e8400-e29b-41d4-a716-446655440001'),
('post-3', 'Bob Post', '550e8400-e29b-41d4-a716-446655440002');在测试中加载:
typescript
import path from 'path';
beforeAll(async () => {
({ pg, db, teardown } = await getConnections());
await pg.loadSql([
path.join(__dirname, 'fixtures/seed.sql')
]);
});加载多个 SQL 文件:
typescript
await pg.loadSql([
path.join(__dirname, 'fixtures/users.sql'),
path.join(__dirname, 'fixtures/posts.sql'),
path.join(__dirname, 'fixtures/comments.sql')
]);文件按顺序执行,因此需要将父表的加载逻辑放在子表前面。
Seeding with loadCsv()
使用 loadCsv() 植入数据
Best for large datasets or spreadsheet exports.
Create :
__tests__/fixtures/users.csvcsv
id,email,name
550e8400-e29b-41d4-a716-446655440001,alice@example.com,Alice
550e8400-e29b-41d4-a716-446655440002,bob@example.com,Bob
550e8400-e29b-41d4-a716-446655440003,charlie@example.com,CharlieLoad in tests:
typescript
import path from 'path';
beforeAll(async () => {
({ pg, db, teardown } = await getConnections());
await pg.loadCsv({
'app.users': path.join(__dirname, 'fixtures/users.csv'),
'app.posts': path.join(__dirname, 'fixtures/posts.csv')
});
});Important: uses PostgreSQL's COPY command, which bypasses RLS. Always use (superuser) client for CSV loading.
loadCsv()pg最适合大型数据集或电子表格导出数据。
创建 :
__tests__/fixtures/users.csvcsv
id,email,name
550e8400-e29b-41d4-a716-446655440001,alice@example.com,Alice
550e8400-e29b-41d4-a716-446655440002,bob@example.com,Bob
550e8400-e29b-41d4-a716-446655440003,charlie@example.com,Charlie在测试中加载:
typescript
import path from 'path';
beforeAll(async () => {
({ pg, db, teardown } = await getConnections());
await pg.loadCsv({
'app.users': path.join(__dirname, 'fixtures/users.csv'),
'app.posts': path.join(__dirname, 'fixtures/posts.csv')
});
});注意: 使用 PostgreSQL 的 COPY 命令,会绕过 RLS。加载 CSV 时请始终使用 (超级用户)客户端。
loadCsv()pgCombining Seeding Strategies
组合多种植入策略
Mix methods based on data characteristics:
typescript
beforeAll(async () => {
({ pg, db, teardown } = await getConnections());
// 1. Load large reference data from CSV
await pg.loadCsv({
'app.categories': path.join(__dirname, 'fixtures/categories.csv')
});
// 2. Load complex relationships from SQL
await pg.loadSql([
path.join(__dirname, 'fixtures/users-with-roles.sql')
]);
// 3. Add test-specific data inline
await pg.loadJson({
'app.posts': [
{ title: 'Test Post', owner_id: testUserId, category_id: 1 }
]
});
});根据数据特性混合使用不同方法:
typescript
beforeAll(async () => {
({ pg, db, teardown } = await getConnections());
// 1. 从 CSV 加载大型参考数据
await pg.loadCsv({
'app.categories': path.join(__dirname, 'fixtures/categories.csv')
});
// 2. 从 SQL 加载复杂关联数据
await pg.loadSql([
path.join(__dirname, 'fixtures/users-with-roles.sql')
]);
// 3. 行内添加测试专用数据
await pg.loadJson({
'app.posts': [
{ title: 'Test Post', owner_id: testUserId, category_id: 1 }
]
});
});Per-Test Seeding
单测试专属数据植入
When different tests need different data, seed in :
beforeEach()typescript
beforeAll(async () => {
({ pg, db, teardown } = await getConnections());
});
afterAll(async () => {
await teardown();
});
beforeEach(async () => {
await pg.beforeEach();
await db.beforeEach();
});
afterEach(async () => {
await db.afterEach();
await pg.afterEach();
});
describe('empty state tests', () => {
it('handles no data gracefully', async () => {
const result = await db.query('SELECT COUNT(*) FROM app.posts');
expect(result.rows[0].count).toBe('0');
});
});
describe('populated state tests', () => {
beforeEach(async () => {
await pg.loadJson({
'app.posts': [
{ title: 'Test Post', owner_id: userId }
]
});
});
it('finds existing posts', async () => {
const result = await db.query('SELECT COUNT(*) FROM app.posts');
expect(result.rows[0].count).toBe('1');
});
});当不同测试需要不同数据时,可在 中植入数据:
beforeEach()typescript
beforeAll(async () => {
({ pg, db, teardown } = await getConnections());
});
afterAll(async () => {
await teardown();
});
beforeEach(async () => {
await pg.beforeEach();
await db.beforeEach();
});
afterEach(async () => {
await db.afterEach();
await pg.afterEach();
});
describe('空状态测试', () => {
it('可以优雅处理无数据场景', async () => {
const result = await db.query('SELECT COUNT(*) FROM app.posts');
expect(result.rows[0].count).toBe('0');
});
});
describe('有数据状态测试', () => {
beforeEach(async () => {
await pg.loadJson({
'app.posts': [
{ title: 'Test Post', owner_id: userId }
]
});
});
it('可以查询到已存在的帖子', async () => {
const result = await db.query('SELECT COUNT(*) FROM app.posts');
expect(result.rows[0].count).toBe('1');
});
});RLS-Aware Seeding
适配 RLS 的数据植入
When testing RLS, seed with the appropriate client:
typescript
// Bypass RLS for setup (use pg)
await pg.loadJson({
'app.posts': [{ title: 'Admin Post', owner_id: adminId }]
});
// Respect RLS for user operations (use db with context)
db.setContext({
role: 'authenticated',
'request.jwt.claim.sub': userId
});
await db.loadJson({
'app.posts': [{ title: 'User Post', owner_id: userId }]
});测试 RLS 时,使用对应权限的客户端植入数据:
typescript
// 使用 pg 客户端绕过 RLS 完成初始化数据植入
await pg.loadJson({
'app.posts': [{ title: 'Admin Post', owner_id: adminId }]
});
// 使用带上下文的 db 客户端遵循 RLS 执行用户操作
db.setContext({
role: 'authenticated',
'request.jwt.claim.sub': userId
});
await db.loadJson({
'app.posts': [{ title: 'User Post', owner_id: userId }]
});Fixture Organization
测试夹具组织
Recommended structure:
text
__tests__/
├── fixtures/
│ ├── users.csv
│ ├── posts.csv
│ ├── seed.sql
│ └── complex-scenario.sql
├── users.test.ts
├── posts.test.ts
└── rls.test.ts推荐目录结构:
text
__tests__/
├── fixtures/
│ ├── users.csv
│ ├── posts.csv
│ ├── seed.sql
│ └── complex-scenario.sql
├── users.test.ts
├── posts.test.ts
└── rls.test.tsBest Practices
最佳实践
- Use explicit IDs: Makes referential integrity predictable
- Order by dependencies: Parent tables before child tables
- Keep fixtures minimal: Only seed what tests need
- Use for setup: Bypass RLS during seeding
pg - Use for testing: Enforce RLS during assertions
db - Version control fixtures: SQL/CSV files in repo
- 使用显式 ID:保证参照完整性可预期
- 按依赖顺序加载:父表数据优先于子表加载
- 保持夹具轻量化:仅植入测试所需的最小数据量
- 初始化阶段使用 :植入数据时绕过 RLS
pg - 测试阶段使用 :断言阶段强制执行 RLS 校验
db - 测试夹具纳入版本管控:SQL/CSV 文件提交到代码仓库
Troubleshooting
故障排查
| Issue | Solution |
|---|---|
| Foreign key violation | Load parent tables first |
| RLS blocking inserts | Use |
| CSV format errors | Ensure headers match column names |
| Data persists between tests | Check beforeEach/afterEach hooks |
| 问题 | 解决方案 |
|---|---|
| 外键约束报错 | 优先加载父表数据 |
| RLS 阻止插入操作 | 使用 |
| CSV 格式错误 | 确认表头和表列名完全匹配 |
| 测试间数据残留 | 检查 beforeEach/afterEach 钩子逻辑 |
References
参考资料
- Related skill: for RLS testing patterns
pgsql-test-rls - Related skill: for handling errors
pgsql-test-exceptions - Related skill: for general test setup
pgpm-testing
- 相关技能:RLS 测试模式
pgsql-test-rls - 相关技能:错误处理
pgsql-test-exceptions - 相关技能:通用测试配置
pgpm-testing