测试概览
测试是数据库开发中一个关键的环节,尤其是在使用像行级安全(RLS)策略这样的功能时。这个指南提供了一个全面的方法来测试你的 Supabase 数据库。
🌐 Testing is a critical part of database development, especially when working with features like Row Level Security (RLS) policies. This guide provides a comprehensive approach to testing your Supabase database.
测试方法 #
🌐 Testing approaches
使用 pgTAP 进行数据库单元测试 #
🌐 Database unit testing with pgTAP
pgTAP 是一个用于 Postgres 的单元测试框架,可以进行以下测试:
- 数据库结构:表格、列、约束
- 行级安全 (RLS) 策略
- 函数和过程
- 数据完整性
这个例子演示了如何为一个基础的待办事项应用设置和测试 RLS 策略:
🌐 This example demonstrates setting up and testing RLS policies for a basic todo application:
-
创建一个启用了 RLS 的测试表:
-- Create a todos tablecreate table todos (id uuid primary key default gen_random_uuid(),task text not null,user_id uuid references auth.users not null,completed boolean default false);-- Enable RLSalter table todos enable row level security;-- Create a policycreate policy "Users can only access their own todos"on todos for all -- this policy applies to all operationsto authenticatedusing ((select auth.uid()) = user_id); -
设置你的测试环境:
# Create a new test for our policies using supabase clisupabase test new todos_rls.test -
写下你的 RLS 测试:
begin;-- install tests utilities-- install pgtap extension for testingcreate extension if not exists pgtap with schema extensions;-- Start declare we'll have 4 test cases in our test suiteselect plan(4);-- Setup our testing data-- Set up auth.users entriesinsert into auth.users (id, email) values('123e4567-e89b-12d3-a456-426614174000', 'user1@test.com'),('987fcdeb-51a2-43d7-9012-345678901234', 'user2@test.com');-- Create test todosinsert into public.todos (task, user_id) values('User 1 Task 1', '123e4567-e89b-12d3-a456-426614174000'),('User 1 Task 2', '123e4567-e89b-12d3-a456-426614174000'),('User 2 Task 1', '987fcdeb-51a2-43d7-9012-345678901234');-- as User 1set local role authenticated;set local request.jwt.claim.sub = '123e4567-e89b-12d3-a456-426614174000';-- Test 1: User 1 should only see their own todosselect results_eq('select count(*) from todos',ARRAY[2::bigint],'User 1 should only see their 2 todos');-- Test 2: User 1 can create their own todoselect lives_ok($$insert into todos (task, user_id) values ('New Task', '123e4567-e89b-12d3-a456-426614174000'::uuid)$$,'User 1 can create their own todo');-- as User 2set local request.jwt.claim.sub = '987fcdeb-51a2-43d7-9012-345678901234';-- Test 3: User 2 should only see their own todosselect results_eq('select count(*) from todos',ARRAY[1::bigint],'User 2 should only see their 1 todo');-- Test 4: User 2 cannot modify User 1's todoSELECT results_ne($$ update todos set task = 'Hacked!' where user_id = '123e4567-e89b-12d3-a456-426614174000'::uuid returning 1 $$,$$ values(1) $$,'User 2 cannot modify User 1 todos');select * from finish();rollback; -
运行测试:
supabase test dbpsql:todos_rls.test.sql:4: NOTICE: extension "pgtap" already exists, skipping./todos_rls.test.sql .. okAll tests successful.Files=1, Tests=6, 0 wallclock secs ( 0.01 usr + 0.00 sys = 0.01 CPU)Result: PASS
应用级测试 #
🌐 Application-Level testing
通过应用代码进行测试可以提供端到端的验证。与使用 pgTAP 的数据库级测试不同,应用级测试无法使用事务来实现隔离。
🌐 Testing through application code provides end-to-end verification. Unlike database-level testing with pgTAP, application-level tests cannot use transactions for isolation.
应用级测试不应该依赖干净的数据库状态,因为在每次测试前重置数据库会很慢,并且会让测试并行化变得困难。相反,你可以通过为每个测试用例使用独特的用户ID来设计独立的测试。
🌐 Application-level tests should not rely on a clean database state, as resetting the database before each test can be slow and makes tests difficult to parallelize. Instead, design your tests to be independent by using unique user IDs for each test case.
这里有一个使用 TypeScript 的例子,它与上面的 pgTAP 测试相呼应:
🌐 Here's an example using TypeScript that mirrors the pgTAP tests above:
import crypto from 'crypto'import { createClient } from '@supabase/supabase-js'import { beforeAll, describe, expect, it } from 'vitest'describe('Todos RLS', () => { // Generate unique IDs for this test suite to avoid conflicts with other tests const USER_1_ID = crypto.randomUUID() const USER_2_ID = crypto.randomUUID() const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLISHABLE_KEY!) beforeAll(async () => { // Setup test data specific to this test suite const adminSupabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SECRET_KEY!) // Create test users with unique IDs await adminSupabase.auth.admin.createUser({ id: USER_1_ID, email: `user1-${USER_1_ID}@test.com`, password: 'password123', // We want the user to be usable right away without email confirmation email_confirm: true, }) await adminSupabase.auth.admin.createUser({ id: USER_2_ID, email: `user2-${USER_2_ID}@test.com`, password: 'password123', email_confirm: true, }) // Create initial todos await adminSupabase.from('todos').insert([ { task: 'User 1 Task 1', user_id: USER_1_ID }, { task: 'User 1 Task 2', user_id: USER_1_ID }, { task: 'User 2 Task 1', user_id: USER_2_ID }, ]) }) it('should allow User 1 to only see their own todos', async () => { // Sign in as User 1 await supabase.auth.signInWithPassword({ email: `user1-${USER_1_ID}@test.com`, password: 'password123', }) const { data: todos } = await supabase.from('todos').select('*') expect(todos).toHaveLength(2) todos?.forEach((todo) => { expect(todo.user_id).toBe(USER_1_ID) }) }) it('should allow User 1 to create their own todo', async () => { await supabase.auth.signInWithPassword({ email: `user1-${USER_1_ID}@test.com`, password: 'password123', }) const { error } = await supabase.from('todos').insert({ task: 'New Task', user_id: USER_1_ID }) expect(error).toBeNull() }) it('should allow User 2 to only see their own todos', async () => { // Sign in as User 2 await supabase.auth.signInWithPassword({ email: `user2-${USER_2_ID}@test.com`, password: 'password123', }) const { data: todos } = await supabase.from('todos').select('*') expect(todos).toHaveLength(1) todos?.forEach((todo) => { expect(todo.user_id).toBe(USER_2_ID) }) }) it('should prevent User 2 from modifying User 1 todos', async () => { await supabase.auth.signInWithPassword({ email: `user2-${USER_2_ID}@test.com`, password: 'password123', }) // Attempt to update the todos we shouldn't have access to // result will be a no-op await supabase.from('todos').update({ task: 'Hacked!' }).eq('user_id', USER_1_ID) // Log back in as User 1 to verify their todos weren't changed await supabase.auth.signInWithPassword({ email: `user1-${USER_1_ID}@test.com`, password: 'password123', }) // Fetch User 1's todos const { data: todos } = await supabase.from('todos').select('*') // Verify that none of the todos were changed to "Hacked!" expect(todos).toBeDefined() todos?.forEach((todo) => { expect(todo.task).not.toBe('Hacked!') }) })})测试隔离策略 #
🌐 Test isolation strategies
对于应用级测试,可以考虑以下方法来实现测试隔离:
🌐 For application-level testing, consider these approaches for test isolation:
- 唯一标识符:为每个测试套件生成唯一 ID,以防止数据冲突
- 测试后清理:如果需要,在
afterAll或afterEach钩子里清理创建的数据 - 隔离数据集:在数据中使用前缀或命名空间来区分测试用例
持续集成测试 #
🌐 Continuous integration testing
在你的 CI 流水线中设置自动化数据库测试:
🌐 Set up automated database testing in your CI pipeline:
- 创建一个 GitHub Actions 工作流
.github/workflows/db-tests.yml:
name: Database Testson: push: branches: [main] pull_request: branches: [main]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Supabase CLI uses: supabase/setup-cli@v1 - name: Start Supabase run: supabase start - name: Run Tests run: supabase test db最佳实践 #
🌐 Best practices
- 测试数据设置
- 使用 begin 和 rollback 来确保测试隔离
- 创建涵盖边缘情况的真实测试数据
- 在测试中使用不同的用户角色和权限
- RLS 政策测试
- 测试创建、读取、更新、删除操作
- 测试不同的用户角色:匿名用户和已认证用户
- 测试边缘情况和潜在的安全绕过
- 总是测试负面情况:用户不应该能够做的事情
- CI/CD 集成
- 在每次拉取请求时自动运行测试
- 在部署流程中加入数据库测试
- 使用事务让测试运行更快
现实世界的例子 #
🌐 Real-World examples
想了解更复杂、真实案例的数据库测试,可以看看:
🌐 For more complex, real-world examples of database testing, check out:
- 数据库测试示例仓库 - 一个用于测试 RLS 策略的生产级示例
- RLS 指南和最佳实践
故障排除 #
🌐 Troubleshooting
常见问题及解决方法:
🌐 Common issues and solutions:
- 由于 RLS 导致的测试失败
- 确保你已经设置了正确的角色
set local role authenticated; - 验证 JWT 声明是否已设置
set local "request.jwt.claims" - 检查策略定义是否符合你的测试假设
- 确保你已经设置了正确的角色
- CI 流水线问题
- 确认Supabase CLI安装正确
- 确保在测试前运行数据库迁移
- 使用事务检查测试是否隔离得当
额外资源 #
🌐 Additional resources