You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Vitest中Mock Nuxt3项目里的Supabase?

Nuxt3 + Supabase Vitest单元测试Mock报错问题

问题背景

我有一个Nuxt3应用,使用@nuxtjs/supabase模块实现认证与存储功能,目前正尝试为其添加Vitest单元测试,但Mock Supabase时始终报错。

代码片段

app.vue

<script lang="ts" setup>
  import type { Database } from '@/types';

  const supabase = useSupabaseClient<Database>();
  supabase.auth.onAuthStateChange(async (event) => {
    if (event === 'SIGNED_OUT') {
      await supabase.removeAllChannels();
    }
  });
</script>

<template>
  <div class="bg-white h-full">
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

tests/unit/app.spec.ts

import { mountSuspended } from '@nuxt/test-utils/runtime';
import { describe, expect, it, vi } from 'vitest';
import app from '@/app.vue';

// 尝试过的Mock方式(已注释)
// const supabaseClientMock = vi.fn(() => ({
//   auth: {
//     onAuthStateChange: vi.fn(),
//   },
//   realtime: {
//     removeAllChannels: vi.fn(),
//   },
// }));
// vi.stubGlobal('useSupabaseClient', supabaseClientMock);

// vi.mock('#imports', () => {
//   return {
//     useNuxtApp: () => ({
//       $supabase: {
//         client: {
//           auth: {
//             onAuthStateChange: vi.fn(),
//             signIn: vi.fn(),
//             signOut: vi.fn(),
//           },
//           realtime: {
//             removeAllChannels: vi.fn(),
//           },
//           // Add other properties and methods as needed
//         },
//       },
//     }),
//   };
// });

const supabaseClientMock = vi.fn(() => ({
  auth: {
    onAuthStateChange: vi.fn(),
  },
  realtime: {
    removeAllChannels: vi.fn(),
  },
}));

vi.stubGlobal('useSupabaseClient', supabaseClientMock);

describe('App', async () => {
  describe('App Component', async () => {

    it('renders the app', async () => {
      const wrapper = await mountSuspended(app);
      expect(wrapper.exists())
        .toBe(true);
    });
  });
});

报错信息

TypeError: Cannot read properties of undefined (reading 'auth')
 ❯ setup app.vue:7:7
      5|   supabase.auth.onAuthStateChange(async (event) => {
      6|     if (event === 'SIGNED_OUT') {
      7|       await supabase.removeAllChannels();
       |       ^
      8|     }
      9|   });
 ❯ wrappedSetup node_modules/@nuxt/test-utils/dist/runtime-utils/index.mjs:93:26
 ❯ clonedComponent.setup node_modules/@nuxt/test-utils/dist/runtime-utils/index.mjs:141:48
 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:195:19
 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7572:25
 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7533:36
 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5861:7
 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5827:9
 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5306:11
 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5971:11

This error originated in "tests/unit/app.spec.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
The latest test that might've caused the error is "renders the app". It might mean one of the following:
 - The error was thrown, while Vitest was running this test.
 - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

补充说明

  • 理想情况下希望将Mock配置移至setupFiles中,避免在每个测试文件中重复配置,但目前先解决报错问题即可。
  • 另一个测试文件中已成功Mock Supabase(该文件在工具函数中使用Supabase),代码如下:
vi.mock('@/utils/helpers', () => {
  const buildImageSrc = vi.fn((url: string) => url);

  return {
    useSupabaseClient: () => ({
      storage: {
        from: () => ({
          getPublicUrl: (url: string) => ({
            data: { publicUrl: url },
          }),
        }),
      },
    }),
    buildImageSrc,
  };
});

解决方案

1. 修正Mock结构错误

报错核心原因是removeAllChannels是Supabase Client实例的直接方法,而非挂载在realtime对象下。你当前的Mock把该方法放在realtime中,与实际代码调用逻辑不匹配。

修改后的Mock代码:

const supabaseClientMock = vi.fn(() => ({
  auth: {
    onAuthStateChange: vi.fn(),
  },
  removeAllChannels: vi.fn().mockResolvedValue(undefined), // 直接挂载在client实例上
}));

vi.stubGlobal('useSupabaseClient', supabaseClientMock);

2. 使用Nuxt自动导入的正确Mock方式

对于Nuxt自动导入的函数(如useSupabaseClient),优先使用vi.mockMock#imports,而非stubGlobal,避免Nuxt自动导入的优先级覆盖全局Stub:

vi.mock('#imports', () => ({
  useSupabaseClient: vi.fn(() => ({
    auth: {
      onAuthStateChange: vi.fn(),
    },
    removeAllChannels: vi.fn().mockResolvedValue(undefined),
  })),
}));

3. 全局复用Mock配置

如果要将Mock配置移至setupFiles实现全局复用:

  1. 在vitest.config.ts中添加配置:
// vitest.config.ts
export default defineConfig({
  test: {
    setupFiles: ['./tests/setup/supabase-mock.ts'],
    // 其他测试配置
  },
});
  1. 创建tests/setup/supabase-mock.ts文件:
import { vi } from 'vitest';

vi.mock('#imports', () => ({
  useSupabaseClient: vi.fn(() => ({
    auth: {
      onAuthStateChange: vi.fn(),
    },
    removeAllChannels: vi.fn().mockResolvedValue(undefined),
    // 可添加其他需要Mock的Supabase方法,如storage等
  })),
}));

4. 验证逻辑的测试示例

如果需要测试onAuthStateChange的触发逻辑,可以在Mock中手动调用回调:

it('calls removeAllChannels when signed out', async () => {
  const mockRemoveAllChannels = vi.fn().mockResolvedValue(undefined);
  const mockOnAuthStateChange = vi.fn((callback) => {
    // 触发SIGNED_OUT事件
    callback('SIGNED_OUT');
  });

  vi.mock('#imports', () => ({
    useSupabaseClient: vi.fn(() => ({
      auth: {
        onAuthStateChange: mockOnAuthStateChange,
      },
      removeAllChannels: mockRemoveAllChannels,
    })),
  }));

  await mountSuspended(app);
  expect(mockRemoveAllChannels).toHaveBeenCalled();
});

内容的提问来源于stack exchange,提问作者J. Jackson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 19:24:55