Guides

Testing

Mock reactive sessions in Nuxt component tests and create real sessions for end-to-end tests.

The module provides separate helpers for Nuxt's runtime and end-to-end test environments. Follow Nuxt's testing setup to configure Vitest and install @nuxt/test-utils.

Mocking useUserSession

Use createUserSessionMock with mockNuxtImport to replace useUserSession. It provides reactive user, session, loggedIn, and ready refs, plus the composable's session methods.

mockNuxtImport is hoisted, so create the mock inside vi.hoisted:

tests/nuxt/auth-status.nuxt.spec.ts
import { mockNuxtImport, mountSuspended } from '@nuxt/test-utils/runtime'
import { beforeEach, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import AuthStatus from '~/components/AuthStatus.vue'

const { auth } = await vi.hoisted(async () => {
  const { createUserSessionMock } = await import('@nuxtjs/better-auth/test-utils/runtime')
  return { auth: createUserSessionMock() }
})

mockNuxtImport('useUserSession', () => () => auth)
beforeEach(() => auth.reset())

it('reacts to session changes', async () => {
  const component = await mountSuspended(AuthStatus)
  expect(component.text()).toContain('Signed out')

  const now = new Date()
  auth.setSession({
    user: {
      id: 'user-1', name: 'Viewer', email: 'viewer@example.test',
      emailVerified: true, createdAt: now, updatedAt: now,
    },
    session: {
      id: 'session-1', userId: 'user-1',
      createdAt: now, updatedAt: now,
      expiresAt: new Date(Date.now() + 3600_000),
    },
  })
  await nextTick()
  expect(component.text()).toContain('Signed in')

  await auth.signOut()
  await nextTick()
  expect(component.text()).toContain('Signed out')
  component.unmount()
})

Supply any required additional fields from your auth configuration. reset() restores a copy of the initial session passed to the factory, or the signed-out state when omitted. setSession(null, { ready: false }) models a pending session; waitForSession() waits until the mock is logged in, or for five seconds, just like the real composable. Use Vitest fake timers to test the timeout.

updateUser() merges fields into the mock user. signOut() clears the session and calls an optional onSuccess callback. fetchSession() does nothing by default; replace it with vi.fn() when testing refresh behavior. These methods make no network requests. Mock useSignIn and useSignUp separately when testing login forms.

Creating sessions for end-to-end tests

Use createAuthTestContext with the server started by @nuxt/test-utils/e2e. It installs Better Auth's testUtils plugin and exposes createUser, login, and clear through a private test route. Pass your usual Nuxt setup() options to the factory. It merges your nuxtConfig and env with the test module, selects a local port or uses your port, and configures the matching siteUrl.

Configure a local Better Auth server and an isolated test database in your fixture. For example:

tests/fixtures/auth/server/auth.config.ts
import { memoryAdapter } from 'better-auth/adapters/memory'

export default defineServerAuth({
  database: memoryAdapter({ user: [], session: [], account: [], verification: [] }),
})

Use the login result's headers for HTTP tests and its cookies for Playwright:

tests/e2e/auth.test.ts
import { fileURLToPath } from 'node:url'
import { $fetch, getBrowser, setup, url } from '@nuxt/test-utils/e2e'
import { createAuthTestContext } from '@nuxtjs/better-auth/test-utils/e2e'
import { afterEach, describe, expect, it } from 'vitest'

const auth = await createAuthTestContext({
  rootDir: fileURLToPath(new URL('../fixtures/auth', import.meta.url)),
  browser: true,
})

describe('Authenticated access', async () => {
  await setup(auth.setupOptions)
  afterEach(() => auth.clear())

  it('uses the same session for API requests and pages', async () => {
    const user = await auth.createUser({ name: 'Test viewer' })
    const { headers, cookies } = await auth.login({ userId: user.id })
    const me = await $fetch('/api/me', { headers })
    expect(me.user.id).toBe(user.id)

    const browser = await getBrowser()
    const context = await browser.newContext()
    try {
      await context.addCookies(cookies)
      const page = await context.newPage()
      await page.goto(url('/protected'))
      expect(await page.getByText('Test viewer').textContent()).toBe('Test viewer')
    }
    finally {
      await context.close()
    }
  })
})

This example assumes /api/me returns requireUserSession(event) and /protected displays the user's name. These are real persisted sessions, so server middleware, SSR, hydration, and sign-out use normal authentication. The helpers skip signup and password entry.

Create one auth context per test suite. clear() deletes users created by that context and their sessions; call it after each test, and do not run tests sharing that context concurrently. login only accepts users created through the same context. Close browser contexts separately.

The bridge is installed only by auth.setupOptions in a test build and requires a random token kept in private runtime config. Ordinary builds do not register it. Use this setup for local test fixtures, not deployed previews or production. It requires a local server and does not support clientOnly mode. The helper overrides NUXT_PUBLIC_SITE_URL for the test server, so an inherited application URL cannot change the cookie domain. Pass setup overrides to createAuthTestContext(options), then pass auth.setupOptions unchanged to Nuxt. It requires the built-server mode and rejects dev: true, host, build: false, and server: false. Nuxt Test Utils starts dev servers in a separate CLI process without these module overrides.

login forwards Better Auth's supported login options. Custom session field overrides depend on Better Auth #11217; they are not available in Better Auth 1.7.3. Session fields with configured defaults work with the current release.