测试工具

用于集成测试和端到端测试的测试实用工具

Test Utils 插件为 Better Auth 编写集成测试和端到端测试提供了辅助工具。它包括工厂函数、数据库辅助函数、认证辅助函数以及 OTP 捕获功能。

此插件仅为测试环境设计。它不会添加公共路由,但确实在 ctx.test 上暴露了特权辅助函数。建议将其排除在生产环境的认证配置之外。

安装

将插件添加到仅用于测试的认证配置

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

export const auth = betterAuth({
    // ... 其他配置选项
    plugins: [
        testUtils() 
    ]
})

testUtils() 保持在单独的仅测试认证实例中,可以在不将插件添加到生产认证配置的情况下保留 ctx.test 的类型推断能力。

通过上下文访问测试辅助工具

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

能否包含在生产环境中?

testUtils() 不会注册 HTTP 路由或 API 端点。仅仅将其添加到 plugins 并不会单独创建公开的认证绕过。

然而,它仍会在 ctx.test 上添加特权的服务器端辅助函数。这些辅助函数可以创建会话、持久化用户和组织,并通过认证上下文直接删除记录。当启用 captureOTP: true 时,插件还会安装一个验证钩子,并将 OTP 以内存形式存储以供后续检索。

因此,推荐的做法是将 testUtils 保持在生产环境认证配置之外,并从诸如 auth.test.ts 或专用测试认证工厂这样的单独仅测试认证实例中添加。这样可以在测试中保留这些辅助功能,而不会将其作为生产服务器上下文的一部分进行打包或暴露。

TypeScript 注意事项

Better Auth 会从静态定义的插件数组中尽可能准确地推断插件辅助函数的类型。如果你有条件地在 plugins 中展开 testUtils(),TypeScript 可能无法正确推断 ctx.test

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

export const auth = betterAuth({
    plugins: [
        ...(process.env.NODE_ENV === "test"
            ? [testUtils()]
            : [])
    ]
})

如果你无条件地包含 testUtils() 以保留静态类型推断,请将其视为一种便利权衡,而不是推荐的默认做法。它仍不会暴露公共路由,但你应避免在生产代码路径中使用 ctx.test

用法

工厂函数

工厂函数创建对象但不写入数据库。用于生成具有合理默认值的测试数据。

createUser

创建一个带有默认值(可被覆盖)的用户对象。

// 使用默认值创建用户
const user = test.createUser()
// { id: "...", email: "user-xxx@example.com", name: "Test User", emailVerified: true, ... }

// 使用自定义值创建用户
const user = test.createUser({
    email: "alice@example.com",
    name: "Alice",
    emailVerified: false
})

createOrganization

创建一个组织对象。仅在安装了组织插件时可用。

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

数据库辅助函数

数据库辅助函数可以将测试数据保存到数据库或从数据库中删除数据。

saveUser

将用户保存到数据库。

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

deleteUser

从数据库中删除用户。

await test.deleteUser(user.id)

saveOrganization

将组织保存到数据库。仅在安装了组织插件时可用。

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

deleteOrganization

从数据库中删除组织。仅在安装了组织插件时可用。

await test.deleteOrganization(org.id)

addMember

将用户添加为组织成员。仅在安装了组织插件时可用。

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

认证辅助函数

认证辅助函数用于创建认证会话,以测试受保护路由。

login

为用户创建会话并返回会话详情、请求头、cookies 和令牌。

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

// session - 包含 userId、token 等信息的会话对象
// user - 用户对象
// headers - 带有会话 cookie 的 Headers 对象(用于 fetch/Request)
// cookies - Cookie 数组(用于 Playwright/Puppeteer)
// token - 会话令牌字符串

getAuthHeaders

返回带有会话 cookie 的 Headers 对象,适合进行认证请求。

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

// 与 auth API 一起使用
const session = await auth.api.getSession({ headers })

// 与 fetch 一起使用
const response = await fetch("/api/protected", { headers })

getCookies

返回符合浏览器测试工具(如 Playwright 和 Puppeteer)要求的 cookie 对象数组。

const cookies = await test.getCookies({
    userId: user.id,
    domain: "localhost" // 可选,默认使用 baseURL 域名
})

// Playwright 示例
await context.addCookies(cookies)

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

每个 cookie 对象包含:

  • name - Cookie 名称(例如:"better-auth.session_token")
  • value - Cookie 值
  • domain - Cookie 域
  • path - Cookie 路径(默认为 "/")
  • httpOnly - Cookie 是否为 HttpOnly
  • secure - Cookie 是否需要 HTTPS
  • sameSite - SameSite 属性("Lax"、"Strict" 或 "None")

OTP 捕获

当设置 captureOTP: true 时,插件会被动捕获创建的 OTP。这让你可以在测试中获取 OTP,而无需模拟邮件或短信发送。

OTP 捕获是被动的 - 它不会阻止 OTP 通过配置的 sendVerificationOTP 函数发送。它只是存储一份副本以供测试检索。

auth.test.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 }) {
                // 你的邮件发送逻辑
            }
        })
    ]
})

getOTP

通过标识符(邮箱或手机号)获取捕获的 OTP。

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

// 获取捕获的 OTP
const otp = test.getOTP("user@example.com")
// "123456"

选项

选项类型默认值描述
captureOTPbooleanfalse为测试验证流程启用 OTP 捕获

示例

集成测试(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("应该为认证请求返回用户数据", async () => {
        // 准备数据
        const user = test.createUser({ email: "test@example.com" })
        await test.saveUser(user)

        // 获取认证请求头
        const headers = await test.getAuthHeaders({ userId: user.id })

        // 测试认证请求
        const session = await auth.api.getSession({ headers })
        expect(session?.user.id).toBe(user.id)

        // 清理
        await test.deleteUser(user.id)
    })
})

端到端测试(Playwright)

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

test("仪表盘显示用户名", async ({ context, page }) => {
    const ctx = await auth.$context
    const testUtils = ctx.test

    // 创建并保存用户
    const user = testUtils.createUser({
        email: "e2e@example.com",
        name: "E2E User"
    })
    await testUtils.saveUser(user)

    // 获取 cookies 并注入浏览器
    const cookies = await testUtils.getCookies({
        userId: user.id,
        domain: "localhost"
    })
    await context.addCookies(cookies)

    // 导航至受保护页面
    await page.goto("/dashboard")

    // 断言用户名可见
    await expect(page.getByText("E2E User")).toBeVisible()

    // 清理
    await testUtils.deleteUser(user.id)
})

OTP 验证测试

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

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

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

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

    it("应该使用捕获的 OTP 验证邮箱", async () => {
        const email = "otp-test@example.com"
        const user = test.createUser({ email, emailVerified: false })
        await test.saveUser(user)

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

        // 获取捕获的 OTP
        const otp = test.getOTP(email)
        expect(otp).toBeDefined()

        // 验证邮箱
        await auth.api.verifyEmail({
            body: { email, otp }
        })

        // 清理
        await test.deleteUser(user.id)
    })
})