本地开发

测试概览


测试是数据库开发的关键环节,特别是在使用行级安全(RLS)策略等功能时。本指南提供了测试Supabase数据库的全面方法。

测试方法

使用 pgTAP 进行数据库单元测试

pgTAP 是一个用于 Postgres 的单元测试框架,可以测试:

  • 数据库结构:表、列、约束
  • 行级安全(RLS)策略
  • 函数和存储过程
  • 数据完整性

以下示例演示了如何为一个简单的待办事项应用设置和测试 RLS 策略:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    -- 创建简单的待办事项表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);-- 启用行级安全alter table todos enable row level security;-- 创建策略create policy "用户只能访问自己的待办事项"on todos for all -- 此策略适用于所有操作to authenticatedusing ((select auth.uid()) = user_id);
  2. 设置测试环境:

    1
    2
    # 使用 supabase cli 为我们的策略创建新测试supabase test new todos_rls.test
  3. 编写 RLS 测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    begin;-- 安装测试工具-- 为测试安装 pgtap 扩展create extension if not exists pgtap with schema extensions;-- 声明我们的测试套件将有4个测试用例select plan(4);-- 设置测试数据-- 创建 auth.users 条目insert into auth.users (id, email) values ('123e4567-e89b-12d3-a456-426614174000', 'user1@test.com'), ('987fcdeb-51a2-43d7-9012-345678901234', 'user2@test.com');-- 创建测试待办事项insert into public.todos (task, user_id) values ('用户1的任务1', '123e4567-e89b-12d3-a456-426614174000'), ('用户1的任务2', '123e4567-e89b-12d3-a456-426614174000'), ('用户2的任务1', '987fcdeb-51a2-43d7-9012-345678901234');-- 作为用户1set local role authenticated;set local request.jwt.claim.sub = '123e4567-e89b-12d3-a456-426614174000';-- 测试1: 用户1应该只能看到自己的待办事项select results_eq( 'select count(*) from todos', ARRAY[2::bigint], '用户1应该只能看到自己的2个待办事项');-- 测试2: 用户1可以创建自己的待办事项select lives_ok( $$insert into todos (task, user_id) values ('新任务', '123e4567-e89b-12d3-a456-426614174000'::uuid)$$, '用户1可以创建自己的待办事项');-- 作为用户2set local request.jwt.claim.sub = '987fcdeb-51a2-43d7-9012-345678901234';-- 测试3: 用户2应该只能看到自己的待办事项select results_eq( 'select count(*) from todos', ARRAY[1::bigint], '用户2应该只能看到自己的1个待办事项');-- 测试4: 用户2不能修改用户1的待办事项SELECT results_ne( $$ update todos set task = '被黑了!' where user_id = '123e4567-e89b-12d3-a456-426614174000'::uuid returning 1 $$, $$ values(1) $$, '用户2不能修改用户1的待办事项');select * from finish();rollback;
  4. 运行测试:

    1
    2
    3
    4
    5
    6
    supabase test dbpsql:todos_rls.test.sql:4: NOTICE: extension "pgtap" already exists, skipping./todos_rls.test.sql .. ok所有测试通过。文件数=1, 测试数=6, 0 wallclock secs ( 0.01 usr + 0.00 sys = 0.01 CPU)结果: 通过

应用层测试

通过应用代码进行测试可以提供端到端的验证。与使用 pgTAP 的数据库级测试不同,应用层测试无法使用事务来实现隔离。

以下是一个使用 TypeScript 的示例,与上述 pgTAP 测试相对应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { } from '@supabase/supabase-js'import { , , , } from 'vitest'import from 'crypto'('Todos RLS', () => { // 为当前测试套件生成唯一ID,避免与其他测试冲突 const = .() const = .() const = (..!, ..!) (async () => { // 设置当前测试套件专用的测试数据 const = (..!, ..!) // 使用唯一ID创建测试用户 await ...({ : , : `user1-${}@test.com`, : 'password123', // 我们希望用户无需邮件确认即可立即使用 : true, }) await ...({ : , : `user2-${}@test.com`, : 'password123', : true, }) // 创建初始待办事项 await .('todos').([ { : '用户1的任务1', : }, { : '用户1的任务2', : }, { : '用户2的任务1', : }, ]) }) ('应允许用户1仅查看自己的待办事项', async () => { // 以用户1身份登录 await ..({ : `user1-${}@test.com`, : 'password123', }) const { : } = await .('todos').('*') ().(2) ?.(() => { (.).() }) }) ('应允许用户1创建自己的待办事项', async () => { await ..({ : `user1-${}@test.com`, : 'password123', }) const { } = await .('todos').({ : '新任务', : }) ().() }) ('应允许用户2仅查看自己的待办事项', async () => { // 以用户2身份登录 await ..({ : `user2-${}@test.com`, : 'password123', }) const { : } = await .('todos').('*') ().(1) ?.(() => { (.).() }) }) ('应阻止用户2修改用户1的待办事项', async () => { await ..({ : `user2-${}@test.com`, : 'password123', }) // 尝试更新我们无权访问的待办事项 // 结果将是无操作 await .('todos').({ : '被黑了!' }).('user_id', ) // 重新以用户1身份登录验证其待办事项未被修改 await ..({ : `user1-${}@test.com`, : 'password123', }) // 获取用户1的待办事项 const { : } = await .('todos').('*') // 验证没有待办事项被修改为"被黑了!" ().() ?.(() => { (.)..('被黑了!') }) })})

测试隔离策略

对于应用级测试,可采用以下测试隔离方法:

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

持续集成测试

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

  1. 创建GitHub Actions工作流.github/workflows/db-tests.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
name: 数据库测试on: push: branches: [main] pull_request: branches: [main]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: 安装Supabase CLI uses: supabase/setup-cli@v1 - name: 启动Supabase run: supabase start - name: 运行测试 run: supabase test db

最佳实践

  1. 测试数据准备

    • 使用事务开始和回滚确保测试隔离
    • 创建覆盖边界条件的真实测试数据
    • 在测试中使用不同用户角色和权限
  2. RLS策略测试

    • 测试创建、读取、更新、删除操作
    • 使用不同用户角色测试:匿名和认证用户
    • 测试边界条件和潜在的安全绕过
    • 始终测试负面用例:用户不应该能执行的操作
  3. CI/CD集成

    • 在每个拉取请求上自动运行测试
    • 在部署流水线中包含数据库测试
    • 使用事务保持测试运行速度

实际案例

查看更复杂的真实世界数据库测试案例:

故障排除

常见问题及解决方案:

  1. RLS导致的测试失败

    • 确保设置了正确的角色 set local role authenticated;
    • 验证JWT声明是否设置 set local "request.jwt.claims"
    • 检查策略定义是否符合测试假设
  2. CI流水线问题

    • 确认Supabase CLI已正确安装
    • 确保在测试前运行了数据库迁移
    • 检查是否使用事务正确隔离测试

其他资源