Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/Encore/第15章

第15章:测试与前端集成 —— encore test 与类型化客户端

5 分钟 · 更新于 2026-09-02

后端的两端收口:向内,encore test 提供"真基础设施、零 mock"的集成测试环境;向外,encore gen client 把 API 契约编译成前端可用的类型化 SDK。两者共享同一来源——应用模型。


一、encore test:为什么不是普通 vitest

Encore 用标准 Vitest 作为测试运行器,但入口必须是 encore test

bash
npm install -D vitest
json
// package.json
{
  "scripts": { "test": "vitest" }
}
bash
encore test              # 正确:先建基础设施绑定再跑 vitest
encore test url/         # 指定目录

encore test 相比裸 vitest 多做的事:

  • 自动创建隔离的测试数据库并执行迁移;
  • 建立运行时绑定(数据库连接、Pub/Sub 本地实现、secret 解析);
  • 每个测试文件的数据库操作在事务中执行、结束后回滚——测试之间天然隔离,不需要手写清库(保险起见关键套件仍可 beforeEach 清表)。

裸跑 vitest 的症状 直接 npx vitest 时所有涉及数据库/原语的测试报连接错误或悬挂。本机 B2B 项目把「测试必须 encore test,禁裸 vitest」写成项目硬规则——人和 AI 助手都会犯这个错(AI 尤甚,第 20 章)。

二、测试哲学:mock 外部、不 mock 基础设施

官方指导原则浓缩成一句话:测试直接调用 API 函数,基础设施用真的,外部依赖才 mock

对象策略
自己的 API 端点直接当函数调(await shorten({url})
数据库 / Pub/Sub / 缓存用真的(encore test 已备好)
服务间调用正常发生(真调用)
第三方 API、邮件、短信mock(vi.mock / vi.spyOn
鉴权上下文mock getAuthData(第 10 章)

对比传统"repository 打桩、service mock 三层"的单测:Encore 的测试更接近集成测试,但成本与单测一样低——因为基础设施是框架一键起的。

三、五组测试模式

端点基本测试

typescript
import { describe, it, expect } from "vitest";
import { getUser } from "./api";

describe("getUser", () => {
  it("returns the user by ID", async () => {
    const user = await getUser({ id: "123" });
    expect(user.id).toBe("123");
  });
});

数据库读写闭环

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { createUser, getUser, db } from "./user";

describe("user operations", () => {
  beforeEach(async () => {
    await db.exec`DELETE FROM users`;   // 显式清表(回滚之外的双保险)
  });

  it("creates and retrieves a user", async () => {
    const created = await createUser({ email: "t@example.com", name: "Test" });
    const retrieved = await getUser({ id: created.id });
    expect(retrieved.email).toBe("t@example.com");
  });
});

错误路径

typescript
import { APIError } from "encore.dev/api";

it("throws NotFound for missing user", async () => {
  await expect(getUser({ id: "nonexistent" })).rejects.toThrow("user not found");
});

it("throws with correct error code", async () => {
  try {
    await getUser({ id: "nonexistent" });
  } catch (error) {
    expect(error).toBeInstanceOf(APIError);
    expect((error as APIError).code).toBe("not_found");
  }
});

Pub/Sub 与 Cron

typescript
// Pub/Sub:验证能发布(handler 逻辑单独直调测试)
it("publishes order paid event", async () => {
  const messageId = await orderPaid.publish({ orderNo: "ORD1", amountCents: 1, paidAt: "..." });
  expect(messageId).toBeDefined();
});

// Cron:测被调度的函数本身,不测调度
it("removes expired sessions", async () => {
  await createExpiredSession();
  await cleanupExpiredSessions();   // 直接调 Cron 端点函数
  expect(await countSessions()).toBe(0);
});

vitest 配置

typescript
// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
    include: ["**/*.test.ts"],
    coverage: { reporter: ["text", "json", "html"] },
  },
});

四、encore gen client:类型化前端 SDK

生成

bash
encore gen client <app-id> --output=./web/src/client.ts --env=local
# --env=staging / production  按环境生成(基地址与 API 版本对应该环境)
# --lang=typescript|javascript|go|openapi   通常由 --output 扩展名自动推断
# --services=order,site       只生成部分服务
# --lang=openapi --output=./openapi.json   导出 OpenAPI 规范(实验性)

工程化:挂进 npm script,接口一变就重新生成——后端改签名 → 前端编译失败,接口对齐前置到编译期(第 6 章服务间调用的前端版)。

json
{
  "scripts": {
    "gen": "encore gen client my-app-abc2 --output=./web/src/client.ts --env=local"
  }
}

使用

typescript
import Client, { Local } from "./client";

const client = new Client(Local);                       // 本地
// const client = new Client("https://api.example.com"); // 自定义地址
// const client = new Client(Environment("staging"));    // 按环境名

// 每个服务是对象、每个 API 是方法,类型与后端接口同源
const site = await client.site.add({ url: "https://encore.dev" });
const status = await client.monitor.status();

鉴权凭据

typescript
// 静态凭据
const client = new Client(Local, {
  auth: { authorization: `Bearer ${token}` },
});

// 动态凭据:每次请求前调用
const client = new Client(Local, {
  auth: async () => ({ authorization: `Bearer ${await refreshToken()}` }),
});

错误处理

后端抛的 APIError 在客户端反序列化为同构的 APIError,附类型守卫:

typescript
import { isAPIError, ErrCode } from "./client";

try {
  await client.order.getOrder({ id });
} catch (err) {
  if (isAPIError(err) && err.code === ErrCode.NotFound) {
    showNotFound();
  }
}

流式端点的客户端用法见第 11 章第六节(for await + stream.send)。

五、框架接入示例

React + TanStack Query

tsx
import { useQuery } from "@tanstack/react-query";
import Client, { Local } from "../client";

const client = new Client(Local);

export function SiteStatus() {
  const { data, isLoading, error } = useQuery({
    queryKey: ["status"],
    queryFn: () => client.monitor.status(),
    refetchInterval: 30_000,
  });

  if (isLoading) return <div>加载中…</div>;
  if (error) return <div>出错了</div>;
  return <ul>{data.sites.map(s => <li key={s.id}>{s.id}: {s.up ? "✅" : "❌"}</li>)}</ul>;
}

Next.js 服务端组件

tsx
// app/status/page.tsx
import Client from "@/lib/client";

const client = new Client(process.env.API_URL!);

export default async function StatusPage() {
  const status = await client.monitor.status();   // 服务端直调
  return <StatusTable sites={status.sites} />;
}

环境变量约定

bash
# Vite
VITE_API_URL=http://localhost:4000
# Next.js
NEXT_PUBLIC_API_URL=http://localhost:4000

别忘了 CORS(第 12 章):前端域名要进 allow_origins_with_credentials(凭据请求)。

部署形态二选一

形态做法适用
前后端同域前端构建产物用 api.static + /!path 挂在后端根路径简单部署、无跨域问题(本机 B2B 项目采用)
前后端分离前端上 CDN/Pages,走生成客户端调 API前端独立发版、多端共用 API

六、常见误区

  1. 裸跑 vitest(再强调一次)。
  2. 给数据库/Topic 写 mock——放着真基础设施不用,测试保真度反而下降。
  3. 生成的 client.ts 手改——它是产物,重新生成即覆盖;定制走构造参数(auth/fetcher)。
  4. client 不随后端接口更新重新生成——前端类型静默过期,等运行时才发现。
  5. 前端直连数据库或绕过 client 手写 fetch 调私有 API——私有 API 前端本来就调不到,公开 API 用 client 才有类型。
  6. 测试断言 queryRow 泛型字段名——泛型是断言不是保证(第 5 章),列名改了测试才是防线。

七、实践练习

  1. 给第 14 章 uptime 项目补测试:site CRUD 闭环、check 写入 checks 表、状态翻转发布事件(handler 直调)。
  2. 故意裸跑 npx vitest,记录报错信息——以后见到这个错第一反应是"入口错了"。
  3. 生成 TS 客户端,写一个 Node 脚本轮询 /status 并彩色打印;改后端 SiteStatus 字段名,观察脚本编译失败。
  4. 用 TanStack Query 给 uptime 前端加 30 秒自动刷新的状态页。
  5. 把练习 4 的前端 build 后用 api.static 挂到后端根路径,实现单镜像部署。

八、总结

  1. encore test = vitest + 自动测试库 + 运行时绑定 + 事务回滚隔离;裸 vitest 必挂。
  2. 测试哲学:直调 API 函数、真基础设施、只 mock 外部依赖与鉴权上下文。
  3. 五组模式覆盖端点/数据库/错误/PubSub/Cron;Cron 测函数不测调度。
  4. encore gen client 按环境生成类型化 SDK:服务=对象、API=方法、错误=APIError 同构。
  5. 前端接入三件事:环境变量管基地址、auth 选项管凭据、CORS 管跨域;同域与分离两种部署形态按需选。

请继续阅读:第16章:部署


原始资料引用



本章目录
一、encore test:为什么不是普通 vitest二、测试哲学:mock 外部、不 mock 基础设施三、五组测试模式四、encore gen client:类型化前端 SDK五、框架接入示例六、常见误区七、实践练习八、总结原始资料引用Related Documents
苏ICP备2025204887号-2