BETTER-AUTH.

Test Utils

Testing utilities for integration and E2E testing

The Test Utils plugin provides helpers for writing integration and E2E tests against Better Auth. It includes factories, database helpers, authentication helpers, and OTP capture functionality.

This plugin is designed for test environments only. Do not use it in production.

Installation

Add the plugin to your auth config

auth.ts
import { betterAuth } from "better-auth"
import { testUtils } from "better-auth/plugins"

export const auth = betterAuth({
    // ... other config options
    plugins: [
        testUtils() 
    ]
})

Access test helpers via context

test-setup.ts
const ctx = await auth.$context
const test = ctx.test

Usage

Factories

Factories create objects without writing to the database. Use them to generate test data with sensible defaults.

createUser

Creates a user object with default values that can be overridden.

// Create user with defaults
const user = test.createUser()
// { id: "...", email: "user-xxx@example.com", name: "Test User", emailVerified: true, ... }

// Create user with overrides
const user = test.createUser({
    email: "alice@example.com",
    name: "Alice",
    emailVerified: false
})

createOrganization

Creates an organization object. Only available when the organization plugin is installed.

const org = test.createOrganization({
    name: "Acme Corp",
    slug: "acme-corp"
})

Database Helpers

Database helpers persist and remove test data from the database.

saveUser

Saves a user to the database.

const user = test.createUser({ email: "test@example.com" })
const savedUser = await test.saveUser(user)

deleteUser

Deletes a user from the database.

await test.deleteUser(user.id)

saveOrganization

Saves an organization to the database. Only available with the organization plugin.

const org = test.createOrganization({ name: "Test Org" })
const savedOrg = await test.saveOrganization(org)

deleteOrganization

Deletes an organization from the database. Only available with the organization plugin.

await test.deleteOrganization(org.id)

addMember

Adds a user as a member of an organization. Only available with the organization plugin.

const member = await test.addMember({
    userId: user.id,
    organizationId: org.id,
    role: "admin"
})

Auth Helpers

Auth helpers create authenticated sessions for testing protected routes.

login

Creates a session for a user and returns session details, headers, cookies, and token.

const { session, user, headers, cookies, token } = await test.login({
    userId: user.id
})

// session - The session object with userId, token, etc.
// user - The user object
// headers - Headers object with session cookie (for fetch/Request)
// cookies - Cookie array (for Playwright/Puppeteer)
// token - The session token string

getAuthHeaders

Returns a Headers object with the session cookie set. Useful for making authenticated requests.

const headers = await test.getAuthHeaders({ userId: user.id })

// Use with auth API
const session = await auth.api.getSession({ headers })

// Use with fetch
const response = await fetch("/api/protected", { headers })

getCookies

Returns an array of cookie objects compatible with browser testing tools like Playwright and Puppeteer.

const cookies = await test.getCookies({
    userId: user.id,
    domain: "localhost" // optional, defaults to baseURL domain
})

// Playwright example
await context.addCookies(cookies)

// Puppeteer example
for (const cookie of cookies) {
    await page.setCookie(cookie)
}

Each cookie object contains:

  • name - Cookie name (e.g., "better-auth.session_token")
  • value - Cookie value
  • domain - Cookie domain
  • path - Cookie path (defaults to "/")
  • httpOnly - Whether cookie is HTTP-only
  • secure - Whether cookie requires HTTPS
  • sameSite - SameSite attribute ("Lax", "Strict", or "None")

OTP Capture

When captureOTP: true is set, the plugin passively captures OTPs as they are created. This allows you to retrieve OTPs in tests without needing to mock email or SMS sending.

OTP capture is passive - it does not prevent OTPs from being sent via your configured sendVerificationOTP function. It simply stores a copy for test retrieval.

auth.ts
import { betterAuth } from "better-auth"
import { testUtils, emailOTP } from "better-auth/plugins"

export const auth = betterAuth({
    plugins: [
        testUtils({ captureOTP: true }), 
        emailOTP({
            async sendVerificationOTP({ email, otp }) {
                // Your email sending logic
            }
        })
    ]
})

getOTP

Retrieves a captured OTP by identifier (email or phone number).

// Send OTP
await auth.api.sendVerificationOTP({
    body: { email: "user@example.com", type: "sign-in" }
})

// Retrieve captured OTP
const otp = test.getOTP("user@example.com")
// "123456"

Options

OptionTypeDefaultDescription
captureOTPbooleanfalseEnable OTP capture for testing verification flows

Examples

Integration Test (Vitest)

import { describe, it, expect, beforeAll } from "vitest"
import { auth } from "./auth"
import type { TestHelpers } from "better-auth/plugins"

describe("protected route", () => {
    let test: TestHelpers

    beforeAll(async () => {
        const ctx = await auth.$context
        test = ctx.test
    })

    it("should return user data for authenticated request", async () => {
        // Setup
        const user = test.createUser({ email: "test@example.com" })
        await test.saveUser(user)

        // Get authenticated headers
        const headers = await test.getAuthHeaders({ userId: user.id })

        // Test authenticated request
        const session = await auth.api.getSession({ headers })
        expect(session?.user.id).toBe(user.id)

        // Cleanup
        await test.deleteUser(user.id)
    })
})

E2E Test (Playwright)

import { test, expect } from "@playwright/test"
import { auth } from "./auth"

test("dashboard shows user name", async ({ context, page }) => {
    const ctx = await auth.$context
    const testUtils = ctx.test

    // Create and save user
    const user = testUtils.createUser({
        email: "e2e@example.com",
        name: "E2E User"
    })
    await testUtils.saveUser(user)

    // Get cookies and inject into browser
    const cookies = await testUtils.getCookies({
        userId: user.id,
        domain: "localhost"
    })
    await context.addCookies(cookies)

    // Navigate to protected page
    await page.goto("/dashboard")

    // Assert user name is visible
    await expect(page.getByText("E2E User")).toBeVisible()

    // Cleanup
    await testUtils.deleteUser(user.id)
})

OTP Verification Test

import { describe, it, expect, beforeAll, beforeEach } from "vitest"
import { auth } from "./auth"
import type { TestHelpers } from "better-auth/plugins"

describe("OTP verification", () => {
    let test: TestHelpers

    beforeAll(async () => {
        const ctx = await auth.$context
        test = ctx.test
    })

    beforeEach(() => {
        test.clearOTPs()
    })

    it("should verify email with captured OTP", async () => {
        const email = "otp-test@example.com"
        const user = test.createUser({ email, emailVerified: false })
        await test.saveUser(user)

        // Request OTP
        await auth.api.sendVerificationOTP({
            body: { email, type: "email-verification" }
        })

        // Get captured OTP
        const otp = test.getOTP(email)
        expect(otp).toBeDefined()

        // Verify email
        await auth.api.verifyEmail({
            body: { email, otp }
        })

        // Cleanup
        await test.deleteUser(user.id)
    })
})