Testing
To ensure that your application works as expected we can write tests. Any aspect of Fresh can be tested as a whole together or in isolation. We use Deno’s built-in test runner to write tests.
Testing middlewares
To test middlewares we’re going to create a dummy
app and return the relevant info we want to check in a custom /
handler. This
test assumes the State
object in utils.ts
has text
property.
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");
});
You can extend this pattern for other middlewares. When you have a middleware that adds a header to the returned response, you can assert against that too.
Testing app wrapper or layouts
Both the app wrapper component and layouts can be tested in the same way.
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");
});
Same can be done for layouts.
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");
});
Testing routes and handlers
For testing your route handlers and business logic, you can use the same
App
pattern shown above. Fresh makes it easy to test
individual routes without needing a full build process, as long as they export a
handler:
import { expect } from "@std/expect";
import { App } from "fresh";
import { type State } from "../utils.ts";
// Import actual route handlers
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!");
});
Testing islands
Testing islands requires different approaches for server-side and client-side behavior:
Server-side rendering of islands
You can test that your islands render correctly on the server using the same
App
pattern. Note: this requires a .tsx
file extension
to use JSX:
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();
// Verify the island's initial HTML is present
expect(html).toContain('class="flex gap-8 py-6"');
expect(html).toContain("Counter Test Page");
expect(html).toContain("3");
});
Client-side island interactivity
For testing client-side island behavior (clicks, state changes, etc.), you need a full build and browser environment. You can use the approach similar to Fresh’s own tests:
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 {
// Basic smoke test: verify the island HTML is served
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();
}
});
import { createBuilder, type InlineConfig } from "vite";
import * as path from "@std/path";
// Default Fresh build configuration
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") } },
},
};
// Helper function to create and build the Fresh app
export async function buildFreshApp(config: InlineConfig = FRESH_BUILD_CONFIG) {
const builder = await createBuilder(config);
await builder.buildApp();
return await import("../_fresh/server.js");
}
// Helper function to start a test server
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 };
}
Note: For most applications, testing the server-side rendering is sufficient. Only test client-side interactivity if you have complex island logic that needs verification.