Skip to content
Local Development

测试概览

测试是数据库开发中一个关键的环节,尤其是在使用像行级安全(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:

  1. 创建一个启用了 RLS 的测试表:

    -- Create a todos table
    create 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 RLS
    alter table todos enable row level security;
    -- Create a policy
    create policy "Users can only access their own todos"
    on todos for all -- this policy applies to all operations
    to authenticated
    using ((select auth.uid()) = user_id);
  2. 设置你的测试环境:

    # Create a new test for our policies using supabase cli
    supabase test new todos_rls.test
  3. 写下你的 RLS 测试:

    begin;
    -- install tests utilities
    -- install pgtap extension for testing
    create extension if not exists pgtap with schema extensions;
    -- Start declare we'll have 4 test cases in our test suite
    select plan(4);
    -- Setup our testing data
    -- Set up auth.users entries
    insert into auth.users (id, email) values
    ('123e4567-e89b-12d3-a456-426614174000', 'user1@test.com'),
    ('987fcdeb-51a2-43d7-9012-345678901234', 'user2@test.com');
    -- Create test todos
    insert 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 1
    set local role authenticated;
    set local request.jwt.claim.sub = '123e4567-e89b-12d3-a456-426614174000';
    -- Test 1: User 1 should only see their own todos
    select 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 todo
    select 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 2
    set local request.jwt.claim.sub = '987fcdeb-51a2-43d7-9012-345678901234';
    -- Test 3: User 2 should only see their own todos
    select 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 todo
    SELECT 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;
  4. 运行测试:

    supabase test db
    psql:todos_rls.test.sql:4: NOTICE: extension "pgtap" already exists, skipping
    ./todos_rls.test.sql .. ok
    All 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.

这里有一个使用 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:

  1. 唯一标识符:为每个测试套件生成唯一 ID,以防止数据冲突
  2. 测试后清理:如果需要,在 afterAll 或 afterEach 钩子里清理创建的数据
  3. 隔离数据集:在数据中使用前缀或命名空间来区分测试用例

持续集成测试 #

🌐 Continuous integration testing

在你的 CI 流水线中设置自动化数据库测试:

🌐 Set up automated database testing in your CI pipeline:

  1. 创建一个 GitHub Actions 工作流 .github/workflows/db-tests.yml:
name: Database Tests
on:
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

  1. 测试数据设置
    • 使用 begin 和 rollback 来确保测试隔离
    • 创建涵盖边缘情况的真实测试数据
    • 在测试中使用不同的用户角色和权限
  2. RLS 政策测试
    • 测试创建、读取、更新、删除操作
    • 测试不同的用户角色:匿名用户和已认证用户
    • 测试边缘情况和潜在的安全绕过
    • 总是测试负面情况:用户不应该能够做的事情
  3. CI/CD 集成
    • 在每次拉取请求时自动运行测试
    • 在部署流程中加入数据库测试
    • 使用事务让测试运行更快

现实世界的例子 #

🌐 Real-World examples

想了解更复杂、真实案例的数据库测试,可以看看:

🌐 For more complex, real-world examples of database testing, check out:

故障排除 #

🌐 Troubleshooting

常见问题及解决方法:

🌐 Common issues and solutions:

  1. 由于 RLS 导致的测试失败
    • 确保你已经设置了正确的角色 set local role authenticated;
    • 验证 JWT 声明是否已设置 set local "request.jwt.claims"
    • 检查策略定义是否符合你的测试假设
  2. CI 流水线问题
    • 确认Supabase CLI安装正确
    • 确保在测试前运行数据库迁移
    • 使用事务检查测试是否隔离得当

额外资源 #

🌐 Additional resources