使用Deno+Sinon测试Supabase Edge函数:Stub Storage.from未调用问题
问题排查与解决:Deno + Sinon测试Supabase Edge函数Stub未触发
核心问题根源
你的测试代码存在两个关键问题:
- 实例不共享:测试中创建的
supabaseAdmin实例,和Edge函数里从../_shared/supabaseAdmin.ts导入的实例完全独立。你Stub的是测试本地实例,但通过supabaseAdmin.functions.invoke调用的是部署后的Edge函数,它使用自身的supabaseAdmin实例,自然不会触发你的Stub。 - Stub层级错误:
storage.from()返回的是Bucket对象,真正调用createSignedUrl的是这个Bucket,而非from方法本身。直接Stubfrom的reject不符合函数调用逻辑。
修复步骤
1. 重构Edge函数为可测试的Handler
把Edge函数拆分为业务逻辑函数和入口函数,让业务逻辑支持依赖注入,方便测试时传入Stub实例:
import 'https://deno.land/x/xhr@0.3.0/mod.ts'; import { corsHeaders } from '../_shared/cors.ts'; // 导出可测试的handler函数,接受请求对象和supabase客户端作为参数 export async function handler(req, supabaseAdmin) { const { campaign_id, call_id, speaker, name } = await req.json() const { data, error } = await supabaseAdmin .storage .from('campaigns') .createSignedUrl(`${campaign_id}/calls/${call_id}/audios/${name}`, 60); if (error) { return new Response(JSON.stringify({ error: error.message }), { status: error.status, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } else if (data) { return new Response(JSON.stringify({ data }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } } // 生产环境入口,使用原有的supabaseAdmin实例 import { supabaseAdmin } from '../_shared/supabaseAdmin.ts'; Deno.serve(async (req) => handler(req, supabaseAdmin));
2. 修改测试代码,直接测试Handler逻辑
不再通过functions.invoke调用远程函数,而是直接调用handler,传入Mock请求和Stub过的supabase实例:
import 'https://deno.land/x/xhr@0.3.0/mod.ts'; import sinon from "npm:sinon"; import { assert, assertEquals } from "https://deno.land/std@0.108.0/testing/asserts.ts"; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.23.0' import { StorageApiError } from 'https://esm.sh/@supabase/storage-js@2.5.4'; import { handler } from './path-to-your-edge-function.ts'; // 替换为Edge函数的实际路径 const testStorageObjectNotFound = async () => { // 创建测试用的supabase实例 const testSupabaseAdmin = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', { auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false, }, } ); // 构建Mock Bucket对象,Stub createSignedUrl方法使其抛出错误 const mockBucket = { createSignedUrl: sinon.stub().rejects(new StorageApiError('Object not found', 404)) }; // Stub storage.from方法,让它返回Mock Bucket const stubFrom = sinon.stub(testSupabaseAdmin.storage, "from").returns(mockBucket); // 模拟请求对象 const mockReq = { json: async () => ({ campaign_id: "123", call_id: "456", speaker: "John Doe", name: "audio-file.mp3", }) }; // 直接调用handler const response = await handler(mockReq, testSupabaseAdmin); // 断言响应结果 const errorResponse = await response.json(); assertEquals(response.status, 404); assertEquals(errorResponse.error, 'Object not found'); // 断言Stub被正确调用 assert(stubFrom.withArgs('campaigns').calledOnce); assert(mockBucket.createSignedUrl.calledOnceWith('123/calls/456/audios/audio-file.mp3', 60)); } Deno.test("should return 404 when storage object not found", testStorageObjectNotFound);
扩展测试场景:createSignedUrl抛出StorageApiError
通过修改StorageApiError的参数,可测试不同错误情况:
const testStoragePermissionError = async () => { const testSupabaseAdmin = createClient(/* 配置同前 */); const mockBucket = { createSignedUrl: sinon.stub().rejects(new StorageApiError('Permission denied', 403)) }; const stubFrom = sinon.stub(testSupabaseAdmin.storage, "from").returns(mockBucket); const mockReq = { json: async () => ({ /* 请求体同前 */ }) }; const response = await handler(mockReq, testSupabaseAdmin); const errorResponse = await response.json(); assertEquals(response.status, 403); assertEquals(errorResponse.error, 'Permission denied'); assert(stubFrom.calledOnce); assert(mockBucket.createSignedUrl.calledOnce); } Deno.test("should return 403 when storage permission denied", testStoragePermissionError);
内容的提问来源于stack exchange,提问作者Bloodbee
相关产品推荐
相关产品推荐

