测试概览
测试是数据库开发中一个关键的环节,尤其是在使用像行级安全(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 的测试表:
1-- Create a todos table2create table todos (3id uuid primary key default gen_random_uuid(),4task text not null,5user_id uuid references auth.users not null,6completed boolean default false7);89-- Enable RLS10alter table todos enable row level security;1112-- Create a policy13create policy "Users can only access their own todos"14on todos for all -- this policy applies to all operations15to authenticated16using ((select auth.uid()) = user_id); -
设置你的测试环境:
1# Create a new test for our policies using supabase cli2supabase test new todos_rls.test -
写下你的 RLS 测试:
1begin;2-- install tests utilities3-- install pgtap extension for testing4create extension if not exists pgtap with schema extensions;5-- Start declare we'll have 4 test cases in our test suite6select plan(4);78-- Setup our testing data9-- Set up auth.users entries10insert into auth.users (id, email) values11('123e4567-e89b-12d3-a456-426614174000', 'user1@test.com'),12('987fcdeb-51a2-43d7-9012-345678901234', 'user2@test.com');1314-- Create test todos15insert into public.todos (task, user_id) values16('User 1 Task 1', '123e4567-e89b-12d3-a456-426614174000'),17('User 1 Task 2', '123e4567-e89b-12d3-a456-426614174000'),18('User 2 Task 1', '987fcdeb-51a2-43d7-9012-345678901234');1920-- as User 121set local role authenticated;22set local request.jwt.claim.sub = '123e4567-e89b-12d3-a456-426614174000';2324-- Test 1: User 1 should only see their own todos25select results_eq(26'select count(*) from todos',27ARRAY[2::bigint],28'User 1 should only see their 2 todos'29);3031-- Test 2: User 1 can create their own todo32select lives_ok(33$$insert into todos (task, user_id) values ('New Task', '123e4567-e89b-12d3-a456-426614174000'::uuid)$$,34'User 1 can create their own todo'35);3637-- as User 238set local request.jwt.claim.sub = '987fcdeb-51a2-43d7-9012-345678901234';3940-- Test 3: User 2 should only see their own todos41select results_eq(42'select count(*) from todos',43ARRAY[1::bigint],44'User 2 should only see their 1 todo'45);4647-- Test 4: User 2 cannot modify User 1's todo48SELECT results_ne(49$$ update todos set task = 'Hacked!' where user_id = '123e4567-e89b-12d3-a456-426614174000'::uuid returning 1 $$,50$$ values(1) $$,51'User 2 cannot modify User 1 todos'52);5354select * from finish();55rollback; -
运行测试:
1supabase test db2psql:todos_rls.test.sql:4: NOTICE: extension "pgtap" already exists, skipping3./todos_rls.test.sql .. ok4All tests successful.5Files=1, Tests=6, 0 wallclock secs ( 0.01 usr + 0.00 sys = 0.01 CPU)6Result: 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:
1import crypto from 'crypto'2import { createClient } from '@supabase/supabase-js'3import { beforeAll, describe, expect, it } from 'vitest'45describe('Todos RLS', () => {6 // Generate unique IDs for this test suite to avoid conflicts with other tests7 const USER_1_ID = crypto.randomUUID()8 const USER_2_ID = crypto.randomUUID()910 const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLISHABLE_KEY!)1112 beforeAll(async () => {13 // Setup test data specific to this test suite14 const adminSupabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SECRET_KEY!)1516 // Create test users with unique IDs17 await adminSupabase.auth.admin.createUser({18 id: USER_1_ID,19 email: `user1-${USER_1_ID}@test.com`,20 password: 'password123',21 // We want the user to be usable right away without email confirmation22 email_confirm: true,23 })24 await adminSupabase.auth.admin.createUser({25 id: USER_2_ID,26 email: `user2-${USER_2_ID}@test.com`,27 password: 'password123',28 email_confirm: true,29 })3031 // Create initial todos32 await adminSupabase.from('todos').insert([33 { task: 'User 1 Task 1', user_id: USER_1_ID },34 { task: 'User 1 Task 2', user_id: USER_1_ID },35 { task: 'User 2 Task 1', user_id: USER_2_ID },36 ])37 })3839 it('should allow User 1 to only see their own todos', async () => {40 // Sign in as User 141 await supabase.auth.signInWithPassword({42 email: `user1-${USER_1_ID}@test.com`,43 password: 'password123',44 })4546 const { data: todos } = await supabase.from('todos').select('*')4748 expect(todos).toHaveLength(2)49 todos?.forEach((todo) => {50 expect(todo.user_id).toBe(USER_1_ID)51 })52 })5354 it('should allow User 1 to create their own todo', async () => {55 await supabase.auth.signInWithPassword({56 email: `user1-${USER_1_ID}@test.com`,57 password: 'password123',58 })5960 const { error } = await supabase.from('todos').insert({ task: 'New Task', user_id: USER_1_ID })6162 expect(error).toBeNull()63 })6465 it('should allow User 2 to only see their own todos', async () => {66 // Sign in as User 267 await supabase.auth.signInWithPassword({68 email: `user2-${USER_2_ID}@test.com`,69 password: 'password123',70 })7172 const { data: todos } = await supabase.from('todos').select('*')73 expect(todos).toHaveLength(1)74 todos?.forEach((todo) => {75 expect(todo.user_id).toBe(USER_2_ID)76 })77 })7879 it('should prevent User 2 from modifying User 1 todos', async () => {80 await supabase.auth.signInWithPassword({81 email: `user2-${USER_2_ID}@test.com`,82 password: 'password123',83 })8485 // Attempt to update the todos we shouldn't have access to86 // result will be a no-op87 await supabase.from('todos').update({ task: 'Hacked!' }).eq('user_id', USER_1_ID)8889 // Log back in as User 1 to verify their todos weren't changed90 await supabase.auth.signInWithPassword({91 email: `user1-${USER_1_ID}@test.com`,92 password: 'password123',93 })9495 // Fetch User 1's todos96 const { data: todos } = await supabase.from('todos').select('*')9798 // Verify that none of the todos were changed to "Hacked!"99 expect(todos).toBeDefined()100 todos?.forEach((todo) => {101 expect(todo.task).not.toBe('Hacked!')102 })103 })104})测试隔离策略 #
🌐 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:
1name: Database Tests23on:4 push:5 branches: [main]6 pull_request:7 branches: [main]89jobs:10 test:11 runs-on: ubuntu-latest1213 steps:14 - uses: actions/checkout@v41516 - name: Setup Supabase CLI17 uses: supabase/setup-cli@v11819 - name: Start Supabase20 run: supabase start2122 - name: Run Tests23 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