Skip to content

为确保您的应用按预期工作,我们可以编写测试。Fresh 的任何方面都可以整体测试或单独测试。我们使用 Deno 的内置 测试运行器 来编写测试。

测试中间件

要测试 中间件,我们将创建一个虚拟应用并在自定义的 / 处理器中返回我们要检查的相关信息。此测试假设 utils.ts 中的 State 对象具有 text 属性。

ts
import { expect } from "@std/expect";
import { App } from "fresh";
import { define, type State } from "../utils.ts";

const middleware = define.middleware((ctx) => {
  ctx.state.text = "middleware text";
  return ctx.next();
});

Deno.test("My middleware - sets ctx.state.text", async () => {
  const handler = new App<State>()
    .use(middleware)
    .get("/", (ctx) => {
      return new Response(ctx.state.text || "");
    })
    .handler();

  const res = await handler(new Request("http://localhost"));
  const text = await res.text();

  expect(text).toEqual("middleware text");
});

您可以将此模式扩展到其他中间件。当您有一个向返回的响应添加头的中间件时,您也可以对此进行断言。

测试应用包装器或布局

应用包装器 组件和 布局 都可以以相同的方式测试。

tsx
import { expect } from "@std/expect";
import { App } from "fresh";
import { define, type State } from "../utils.ts";

const AppWrapper = define.layout(function AppWrapper({ Component }) {
  return (
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <title>My App</title>
      </head>
      <body>
        <Component />
      </body>
    </html>
  );
});

Deno.test("App Wrapper - renders title and content", async () => {
  const handler = new App<State>()
    .appWrapper(AppWrapper)
    .get("/", (ctx) => ctx.render(<h1>hello</h1>))
    .handler();

  const res = await handler(new Request("http://localhost"));
  const text = await res.text();

  expect(text).toContain("My App");
  expect(text).toContain("hello");
});

布局也可以做同样的事情。

tsx
import { expect } from "@std/expect";
import { App } from "fresh";
import { define, type State } from "../utils.ts";

const MyLayout = define.layout(function MyLayout({ Component }) {
  return (
    <div>
      <h1>My Layout</h1>
      <Component />
    </div>
  );
});

Deno.test("MyLayout - renders heading and content", async () => {
  const handler = new App<State>()
    .appWrapper(MyLayout)
    .get("/", (ctx) => ctx.render(<h1>hello</h1>))
    .handler();

  const res = await handler(new Request("http://localhost"));
  const text = await res.text();

  expect(text).toContain("My Layout");
  expect(text).toContain("hello");
});

测试路由和处理器

要测试您的路由处理器和业务逻辑,您可以使用上面显示的相同 App 模式。Fresh 使测试单个路由变得容易,无需完整的构建过程,只要它们导出一个处理器:

ts
import { expect } from "@std/expect";
import { App } from "fresh";
import { type State } from "../utils.ts";

// 导入实际的路由处理器
import { handler as apiHandler } from "../routes/api/[name].tsx";

Deno.test("API route returns name", async () => {
  const app = new App<State>()
    .get("/api/:name", apiHandler.GET)
    .handler();

  const response = await app(new Request("http://localhost/api/joe"));
  const text = await response.text();

  expect(text).toEqual("Hello, Joe!");
});

测试 Islands

测试 islands 需要针对服务器端和客户端行为采用不同的方法:

Islands 的服务器端渲染

您可以使用相同的 App 模式测试您的 islands 在服务器上是否正确渲染。注意:这需要 .tsx 文件扩展名才能使用 JSX:

tsx
import { expect } from "@std/expect";
import { App } from "fresh";
import { useSignal } from "@preact/signals";
import { type State } from "../utils.ts";
import Counter from "../islands/Counter.tsx";

function CounterPage() {
  const count = useSignal(3);
  return (
    <div class="p-8">
      <h1>Counter Test Page</h1>
      <Counter count={count} />
    </div>
  );
}

Deno.test("Counter page renders island", async () => {
  const app = new App<State>()
    .get("/counter", (ctx) => {
      return ctx.render(<CounterPage />);
    })
    .handler();

  const response = await app(new Request("http://localhost/counter"));
  const html = await response.text();

  // 验证 island 的初始 HTML 是否存在
  expect(html).toContain('class="flex gap-8 py-6"');
  expect(html).toContain("Counter Test Page");
  expect(html).toContain("3");
});

客户端 Island 交互性

要测试客户端 island 行为(点击、状态变化等),您需要完整的构建和浏览器环境。您可以使用类似于 Fresh 自己测试的方法:

tsx
import { expect } from "@std/expect";
import { buildFreshApp, startTestServer } from "./test-utils.ts";

const app = await buildFreshApp();

Deno.test("Counter island renders correctly", async () => {
  const { server, address } = startTestServer(app);

  try {
    // 基本冒烟测试:验证 island HTML 被服务
    const response = await fetch(`${address}/`);
    const html = await response.text();

    expect(html).toContain('class="flex gap-8 py-6"');
    expect(html).toContain("3");
  } finally {
    await server.shutdown();
  }
});
tsx
import { createBuilder, type InlineConfig } from "vite";
import * as path from "@std/path";

// 默认 Fresh 构建配置
export const FRESH_BUILD_CONFIG: InlineConfig = {
  logLevel: "error",
  root: "./",
  build: { emptyOutDir: true },
  environments: {
    ssr: { build: { outDir: path.join("_fresh", "server") } },
    client: { build: { outDir: path.join("_fresh", "client") } },
  },
};

// 创建和构建 Fresh 应用的辅助函数
export async function buildFreshApp(config: InlineConfig = FRESH_BUILD_CONFIG) {
  const builder = await createBuilder(config);
  await builder.buildApp();
  return await import("../_fresh/server.js");
}

// 启动测试服务器的辅助函数
export function startTestServer(app: {
  default: {
    fetch: (req: Request) => Promise<Response>;
  };
}) {
  const server = Deno.serve({
    port: 0,
    handler: app.default.fetch,
  });

  const { port } = server.addr as Deno.NetAddr;
  const address = `http://localhost:${port}`;

  return { server, address };
}

注意: 对于大多数应用,测试服务器端渲染就足够了。只有当您有需要验证的复杂 island 逻辑时,才测试客户端交互性。