# 测试工具 (/docs/plugins/test-utils)

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



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

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

## 安装 [#安装]

<Steps>
  <Step>
    ### 将插件添加到仅用于测试的认证配置 [#将插件添加到仅用于测试的认证配置]

    ```ts title="auth.test.ts"
    import { betterAuth } from "better-auth"
    import { testUtils } from "better-auth/plugins" // [!code highlight]

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

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

  <Step>
    ### 通过上下文访问测试辅助工具 [#通过上下文访问测试辅助工具]

    ```ts title="test-setup.ts"
    const ctx = await auth.$context
    const test = ctx.test
    ```
  </Step>
</Steps>

## 能否包含在生产环境中？ [#能否包含在生产环境中]

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

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

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

### TypeScript 注意事项 [#typescript-注意事项]

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

```ts title="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 [#createuser]

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

```ts
// 使用默认值创建用户
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 [#createorganization]

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

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

### 数据库辅助函数 [#数据库辅助函数]

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

#### saveUser [#saveuser]

将用户保存到数据库。

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

#### deleteUser [#deleteuser]

从数据库中删除用户。

```ts
await test.deleteUser(user.id)
```

#### saveOrganization [#saveorganization]

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

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

#### deleteOrganization [#deleteorganization]

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

```ts
await test.deleteOrganization(org.id)
```

#### addMember [#addmember]

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

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

### 认证辅助函数 [#认证辅助函数]

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

这三个辅助函数都接受一个可选的 `session` 对象，用于传入通过 `session.additionalFields` 或插件的会话架构配置的字段。提供的值会覆盖默认值；未提供的字段保留其默认值。`id`、`userId`、`token` 和时间戳等标准会话字段会被忽略。

#### login [#login]

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

```ts
const { session, user, headers, cookies, token } = await test.login({
    userId: user.id,
    session: { providerToken: "test-token" } // optional additional fields
})

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

#### getAuthHeaders [#getauthheaders]

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

```ts
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 [#getcookies]

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

```ts
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 捕获 [#otp-捕获]

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

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

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

export const auth = betterAuth({
    plugins: [
        testUtils({ captureOTP: true }), // [!code highlight]
        emailOTP({
            async sendVerificationOTP({ email, otp }) {
                // 你的邮件发送逻辑
            }
        })
    ]
})
```

#### getOTP [#getotp]

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

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

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

## 选项 [#选项]

| 选项           | 类型        | 默认值     | 描述               |
| ------------ | --------- | ------- | ---------------- |
| `captureOTP` | `boolean` | `false` | 为测试验证流程启用 OTP 捕获 |

## 示例 [#示例]

### 集成测试（Vitest） [#集成测试vitest]

```ts
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） [#端到端测试playwright]

```ts
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 验证测试 [#otp-验证测试]

```ts
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)
    })
})
```

