Next.js/T3 Stack集成测试系列1: 基础环境搭建与tRPC后端路由测试

引言

为了业务的完整性,为项目的后端集成测试是非常有必要的。 我在使用 T3 Stack 探索如何为我的应用程序添加测试时遇到了很多问题。 现存网络上关于如何为Next.js的后端路由集成测试的资料非常少且零碎。

由此写下这篇文章帮助更多和我一样的人。本文将通过:

  1. 搭建完整的Vitest测试环境
  2. 编写测试用例 测试Trpc路由
  3. 解决配置过程中遇到的问题

帮助快速建立可靠的测试基础。

环境准备

bash 复制代码
# 使用create-t3-app脚手架初始化项目
pnpm create t3-app@latest my-vitest-tests

脚手架有很多选择,我的选择如下:

  • Will you be using TypeScript or JavaScript?
    • TypeScript
  • Will you be using Tailwind CSS for styling?
    • Yes
  • Would you like to use tRPC?
    • Yes
  • What authentication provider would you like to use?
    • NextAuth.js
  • What database ORM would you like to use?
    • Drizzle
  • Would you like to use Next.js App Router?
    • Yes
  • What database provider would you like to use?
    • PostgreSQL
  • Would you like to use ESLint and Prettier or Biome for linting and formatting?
    • Biome
  • Should we initialize a Git repository and stage the changes?
    • Yes
  • Should we run 'pnpm install' for you?
    • Yes
  • What import alias would you like to use?
    • @

你不需要和我的选择一摸一样,但如果你选择 Prisma 作为你的 database ORM,则代码中的一些示例可能不适用于你,但整体来说,都是通用的,你应该只需要少量的修改。

你应该按照官网的教程先配置好你的项目,配置完成后继续往下看。

目的

初始化项目后,在 src\server\api\routers\post.ts 下默认有一个默认路由 我们的目的是为这个路由编写测试用例,测试这个路由的有效性

安装vitest

我们的目的是为后端trpc路由集成测试,因此不跟随 Next.js 官方的文档文档走

1. 安装vitest

base 复制代码
pnpm add -D vitest

2. 新建 vitest.config.mts 配置文件

typescript 复制代码
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    server: {
      deps: {
        inline: ['next'],
      },
    },
  },
  resolve: {
    alias: {
      '@': '/src',
    },
  },
})

3. 添加基础测试用例

新建 src\test\server\trpc\post.test.ts 文件用于编写测试用例。

typescript 复制代码
import { describe, expect, it } from 'vitest'

describe('post router', () => {
  it('returns the correct greeting', async () => {
    const greeting = ({ text }: { text: string }) => ({
      greeting: `Hello ${text}`,
    })
    const result = greeting({
      text: 'vitest',
    })
    expect(result).toMatchObject({ greeting: 'Hello vitest' })
  })
})

4. 添加执行脚本

json 复制代码
{
  "scripts": {
    // ... 省略
    "test": "vitest"
  }
}

5. 运行 pnpm run test

如果出现上图内容,则 vitest 就安装完成了。

接入真实路由

1. caller

为了调用trpc路由,我们需要一个服务端调用的caller

src\server\api\root.ts 文件下,有一个 createCaller 函数

src\trpc\server.ts 下可以看到

typescript 复制代码
/**
 * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
 * handling a tRPC call from a React Server Component.
 */
const createContext = cache(async () => {
  const heads = new Headers(await headers())
  heads.set('x-trpc-source', 'rsc')

  return createTRPCContext({
    headers: heads,
  })
})

const caller = createCaller(createContext)

只需要传入上下文,即可实现路由的调用

src\server\api\trpc.ts 下有createTRPCContext的定义

typescript 复制代码
export async function createTRPCContext(opts: { headers: Headers }) {
  const session = await auth()

  return {
    db,
    session,
    ...opts,
  }
}

在该函数下添加 createContextInner 用于测试环境下的上下文创建

typescript 复制代码
export function createContextInner(opts: {
  session: Session | null
  db: typeof db
}) {
  const headers = new Headers()
  return {
    db: opts.db,
    session: opts.session,
    headers,
  }
}

测试环境下没有客户端传过来的headers,因此需要手动 new 一个

新建 src\test\server\trpc\utils\setupTrpc.ts 用于创建caller

注意,现在是导入了 @/server/db下的db,调用会出现报错,提示无法连接到数据库,当前仍未解决。 在下一章使用专用数据库会创建专门用于测试的db实例,即可略过这个问题。

typescript 复制代码
import type { Session } from 'next-auth'
import { createCaller } from '@/server/api/root'
import { createContextInner } from '@/server/api/trpc'
import { db } from '@/server/db'

export function setupTrpc() {
  const ctx = createContextInner({
    session: null,
    db,
  })
  const caller = createCaller(ctx)

  return { caller, ctx }
}

interface SetupAuthorizedTrpcProps {
  session?: Session
}

export function setupAuthorizedTrpc({
  session = {
    user: { id: '1', name: 'test' },
    expires: new Date(Date.now() + 1000 * 60 * 60 * 24).toString(),
  },
}: SetupAuthorizedTrpcProps = {}) {
  const ctx = createContextInner({
    session,
    db,
  })
  const caller = createCaller(ctx)

  return { callerAuthorized: caller, ctx }
}

然后在 src\test\server\trpc\post.test.ts 下使用

typescript 复制代码
import { describe, expect, it } from 'vitest'
import { setupTrpc } from './utils/setupTrpc'

describe('post router', async () => {
  it('returns the correct greeting', async () => {
    const { caller } = setupTrpc()
    const result = await caller.post.hello({
      text: 'vitest',
    })
    expect(result).toMatchObject({ greeting: 'Hello vitest' })
  })
})

此时运行vitest,会遇到以下问题

2. env

该错误由t3-env抛出。 这是一个类型安全访问环境变量的方案,由于我们导入了@/server/db下的db,此部分会取出.env中的DATABASE_URL连接数据库。 t3-env 检测到DATABASE_URL未定义,因此抛出错误。

本质上是运行时没有加载环境变量。

当我们使用 next cli 启动项目时,Next.js 内部会自动为我们注入环境变量,但当我们使用vitest启动测试时,需要我们自己手动加载环境变量

我们需要安装@next/env

base 复制代码
pnpm i @next/env -D

并在vitest.config.mts 文件中加载

typescript 复制代码
import nextEnv from '@next/env'
import { defineConfig } from 'vitest/config'

nextEnv.loadEnvConfig(process.cwd())

export default defineConfig({
  test: {
    server: {
      deps: {
        inline: ['next'],
      },
    },
  },
  resolve: {
    alias: {
      '@': '/src',
    },
  },
})

再次运行vitest,运行成功

3. 添加受保护路由测试

typescript 复制代码
it('throws an error if not logged in', async () => {
  const { caller } = setupTrpc()
  await expect(() =>
    caller.post.getSecretMessage(),
  ).rejects.toThrowErrorMatchingInlineSnapshot('[TRPCError: UNAUTHORIZED]')
})

it('returns the secret message if logged in', async () => {
  const { callerAuthorized } = setupAuthorizedTrpc()
  const example = await callerAuthorized.post.getSecretMessage()
  expect(example).toMatchInlineSnapshot(
    `"you can now see this secret message!"`,
  )
})

运行通过

4. 可能出现的问题

在最新的create-t3-app脚手架下创建的项目,上述过程一个可以正常实现。 但如果是在以前创建的t3项目,大概是t3没有升级到react19之前,可能会出现以下问题。

1.Next Auth Error

这可以通过配置vitest的Vite-node server options选项解决

typescript 复制代码
export default defineConfig({
  // ... 省略
  test: {
    server: {
      deps: {
        inline: ['next'],
      },
    },
  },

})

2.err-(0 , cache) is not a function

这似乎会出现在 react canary 版本中,我们可以Vitest的mock功能来模拟React模块,添加这个函数的定义。

创建 src\test\setup.ts 文件,添加以下内容

typescript 复制代码
import { vi } from 'vitest'

vi.mock('react', async (importOriginal) => {
  const testCache = <T extends (...args: Array<unknown>) => unknown>(func: T) =>
    func
  const originalModule = await importOriginal<typeof import('react')>()
  return {
    ...originalModule,
    cache: testCache,
  }
})

然后在 vitest.config.mts 配置一下

typescript 复制代码
export default defineConfig({
  test: {
    setupFiles: ['./src/test/setup.ts'],
    server: {
      deps: {
        inline: ['next'],
      },
    },
  },
  resolve: {
    alias: {
      '@': '/src',
    },
  },
})

该文件现在会在所有测试之前执行。

这应该可以解决问题

总结

经过以上流程,现在已经可以做到对trpc的router进行测试。 不过为了不污染数据库,在测试环境下,我们应该另外使用测试专业数据库进行测试。

请看 Next.js/T3 Stack集成测试系列2 搭建专用测试数据库环境

还没写

相关链接

博客原文

示例仓库

vitest

nextjs test-environment-variables

TypeError: (0 , _react.cache) is not a function #49304

Next Auth Error

相关推荐
浪遏5 天前
我的远程实习(六) | 一个demo讲清Auth.js国外平台登录鉴权👈|nextjs
前端·面试·next.js
yufei-coder7 天前
配置Next.js环境 使用vscode
开发语言·javascript·vscode·next.js
RJiazhen8 天前
极致的网页加载速度——SSR技术调研
前端·next.js
Neolock9 天前
Next.js 中间件鉴权绕过漏洞 (CVE-2025-29927) 复现利用与原理分析
网络·web安全·中间件·cve·next.js
ZhongyiChen10 天前
【NEXT JS 之旅】next-auth 助力实现最小登录方案
前端·后端·next.js
kk欣11 天前
优化前端AI交互模块:从复杂到优雅的蜕变
next.js
kk欣12 天前
优化 Markdown 渲染:从 Next.js MDX 到 React Markdown 的选择之旅
next.js
梦兮林夕12 天前
17 国际化与 Next.js 中的国际化实现
前端·react.js·next.js
栩栩云生12 天前
[250327] Next.js 修复安全漏洞 CVE-2025-29927 | Apache Flink 2.0.0 正式发布
安全·apache·next.js