如何用Vitest模拟Nuxt中的useSanctumAuth组合式函数?
解决Nuxt中间件测试中useNuxtApp未Mock的错误
问题场景
在对接Laravel API的Nuxt.js项目中,使用nuxt-auth-sanctum对接Laravel Fortify,编写了检查用户邮箱是否验证的全局中间件,并尝试用Vitest做TDD测试。
中间件代码
import { defineNuxtRouteMiddleware, navigateTo } from 'nuxt/app' import type { User } from '~/models/user' import { useSanctumAuth } from '#imports' // 因VS Code自动导入识别问题,手动从#imports导入 export default defineNuxtRouteMiddleware(async (to, from) => { const { user } = useSanctumAuth<User>() // 排除不需要验证的路由 const excludedRoutes = ['/auth/login'] if (excludedRoutes.includes(to.path)) { return } // 未验证邮箱则跳转至验证页面 if (user.value && !user.value.email_verified_at) { return navigateTo('/auth/verify-email') } })
测试代码及报错
测试时尝试Mock useSanctumAuth,但运行测试时报错:
[vitest] No "useNuxtApp" export is defined on the "nuxt/app" mock. Did you forget to return it from "vi.mock"?
测试代码:
import { describe, it, expect, vi, beforeEach } from 'vitest' import checkEmailVerified from '@/middleware/check-email-verified.global' import type { RouteLocationNormalized } from 'vue-router' import { navigateTo } from 'nuxt/app' vi.mock('nuxt/app', () => ({ navigateTo: vi.fn(), defineNuxtRouteMiddleware: vi.fn((middleware) => middleware), })) vi.mock('#imports', async () => ({ useSanctumAuth: vi.fn(() => ({ user: { value: { email_verified_at: null, }, }, })), })) describe('checkEmailVerified middleware', () => { beforeEach(() => { vi.clearAllMocks() }) it('should allow access to excluded routes without redirection', async () => { const to: RouteLocationNormalized = { path: '/auth/login', } as RouteLocationNormalized const from: RouteLocationNormalized = { path: '/', } as RouteLocationNormalized await checkEmailVerified(to, from) // 报错触发行 expect(navigateTo).not.toHaveBeenCalled() }) })
错误原因
#imports 是Nuxt提供的虚拟自动导入模块,Vitest对这类虚拟模块的Mock支持有限,导致你的Mock并未真正拦截useSanctumAuth的调用——实际执行了nuxt-auth-sanctum包中真实的useSanctumAuth函数,而该函数内部依赖useNuxtApp,但你Mock的nuxt/app中没有导出这个函数,因此触发错误。
解决方案
方案1:直接Mock nuxt-auth-sanctum 模块(推荐)
useSanctumAuth实际来自nuxt-auth-sanctum包,直接Mock这个模块就能彻底替换真实函数,避免虚拟模块的Mock问题:
import { describe, it, expect, vi, beforeEach } from 'vitest' import checkEmailVerified from '@/middleware/check-email-verified.global' import type { RouteLocationNormalized } from 'vue-router' import { navigateTo } from 'nuxt/app' vi.mock('nuxt/app', () => ({ navigateTo: vi.fn(), defineNuxtRouteMiddleware: vi.fn((middleware) => middleware), // 可选:若后续有其他依赖,可添加useNuxtApp的基础Mock useNuxtApp: vi.fn(() => ({})), })) // 直接Mock nuxt-auth-sanctum模块,替换useSanctumAuth vi.mock('nuxt-auth-sanctum', () => ({ useSanctumAuth: vi.fn(() => ({ user: { value: { email_verified_at: null, }, }, })), })) describe('checkEmailVerified middleware', () => { beforeEach(() => { vi.clearAllMocks() }) it('should allow access to excluded routes without redirection', async () => { const to: RouteLocationNormalized = { path: '/auth/login', } as RouteLocationNormalized const from: RouteLocationNormalized = { path: '/', } as RouteLocationNormalized await checkEmailVerified(to, from) expect(navigateTo).not.toHaveBeenCalled() }) })
方案2:在nuxt/app的Mock中补充useNuxtApp
如果坚持使用#imports的Mock方式,可在nuxt/app的Mock中添加useNuxtApp的导出,满足真实函数的依赖:
vi.mock('nuxt/app', () => ({ navigateTo: vi.fn(), defineNuxtRouteMiddleware: vi.fn((middleware) => middleware), // 添加useNuxtApp的Mock,可根据需求补充内部属性 useNuxtApp: vi.fn(() => ({ $sanctum: {} // 示例:添加sanctum相关属性 })), }))
额外优化建议
- 移除手动导入:去掉中间件中
import { useSanctumAuth } from '#imports'的手动导入,Nuxt的自动导入机制会自动处理,避免测试时的虚拟模块Mock问题。 - 动态Mock返回值:测试不同场景时,可动态修改
useSanctumAuth的返回值,覆盖已验证用户、未登录用户等情况:
it('should redirect unverified user to verify email page', async () => { // 获取Mock的useSanctumAuth函数 const useSanctumAuth = vi.mocked(await import('nuxt-auth-sanctum').then(m => m.useSanctumAuth)) // 修改返回值为未验证用户 useSanctumAuth.mockReturnValue({ user: { value: { email_verified_at: null, }, }, }) const to: RouteLocationNormalized = { path: '/dashboard', } as RouteLocationNormalized await checkEmailVerified(to, {} as RouteLocationNormalized) expect(navigateTo).toHaveBeenCalledWith('/auth/verify-email') })
内容的提问来源于stack exchange,提问作者dust_bg
相关产品推荐
相关产品推荐

