| name | hono-testing |
| description | Hono testing patterns - app.request(), test client, mocking environment, and integration testing strategies |
| skill_version | 1.0.0 |
| updated_at | "2025-01-03T00:00:00.000Z" |
| tags | ["hono","testing","vitest","jest","integration-testing","mocking"] |
| progressive_disclosure | {"entry_point":{"summary":"Testing Hono apps with app.request(), typed test client, and environment mocking","when_to_use":"Writing unit and integration tests for Hono APIs","quick_start":"1. Create app instance 2. Use app.request() or testClient 3. Assert response"},"references":[]} |
| context_limit | 800 |
Hono Testing Patterns
Overview
Hono provides a simple testing approach: create a Request, pass it to your app, and validate the Response. The framework includes a typed test client for even better DX.
Key Features:
- Simple
app.request() API
- Typed test client with full inference
- Environment mocking for Workers
- Works with Vitest, Jest, or any test runner
When to Use This Skill
Use Hono testing when:
- Writing unit tests for route handlers
- Integration testing API endpoints
- Testing middleware behavior
- Mocking Cloudflare Workers bindings
- Validating request/response cycles
Basic Testing
Using app.request()
import { Hono } from "hono";
import { describe, it, expect } from "vitest";
const app = new Hono();
app.get("/hello", (c) => c.text("Hello!"));
app.get("/json", (c) => c.json({ message: "Hello" }));
describe("Basic routes", () => {
it("should return text", async () => {
const res = await app.request("/hello");
expect(res.status).toBe(200);
expect(await res.text()).toBe("Hello!");
});
it("should return JSON", async () => {
const res = await app.request("/json");
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toContain("application/json");
expect(await res.json()).toEqual({ message: "Hello" });
});
});
Request Options
const res = await app.request("/search?q=hono&page=1");
const res = await app.request("/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Alice", email: "alice@example.com" }),
});
const formData = new FormData();
formData.append("name", "Alice");
formData.append("email", "alice@example.com");
const res = await app.request("/users", {
method: "POST",
body: formData,
});
const res = await app.request("/protected", {
headers: {
Authorization: "Bearer token123",
"X-Custom-Header": "value",
},
});
const res = await app.request("/users/123", {
method: "DELETE",
});
Typed Test Client
The test client provides full type inference:
import { Hono } from "hono";
import { testClient } from "hono/testing";
import { describe, it, expect } from "vitest";
const app = new Hono()
.get("/users", (c) => c.json({ users: [] }))
.post("/users", async (c) => {
const body = await c.req.json();
return c.json({ id: "1", ...body }, 201);
})
.get("/users/:id", (c) => {
return c.json({ id: c.req.param("id"), name: "Alice" });
});
describe("Users API", () => {
const client = testClient(app);
it("should list users", async () => {
const res = await client.users.$get();
expect(res.status).toBe(200);
const data = await res.json();
expect(data.users).toEqual([]);
});
it("should create user", async () => {
const res = await client.users.$post({
json: { name: "Alice", email: "alice@example.com" },
});
expect(res.status).toBe(201);
const data = await res.json();
expect(data.name).toBe("Alice");
});
it("should get user by id", async () => {
const res = await client.users[":id"].$get({
param: { id: "123" },
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.id).toBe("123");
});
});
Testing with Validation
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
const app = new Hono();
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
app.post("/users", zValidator("json", createUserSchema), async (c) => {
const data = c.req.valid("json");
return c.json({ id: "1", ...data }, 201);
});
describe("Validation", () => {
it("should accept valid data", async () => {
const res = await app.request("/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Alice",
email: "alice@example.com",
}),
});
expect(res.status).toBe(201);
});
it("should reject invalid email", async () => {
const res = await app.request("/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Alice",
email: "invalid-email",
}),
});
expect(res.status).toBe(400);
});
it("should reject missing name", async () => {
const res = await app.request("/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "alice@example.com",
}),
});
expect(res.status).toBe(400);
});
});
Mocking Environment (Cloudflare Workers)
Mock Bindings
import { Hono } from "hono";
type Bindings = {
DB: D1Database;
KV: KVNamespace;
API_KEY: string;
};
const app = new Hono<{ Bindings: Bindings }>();
app.get("/data", async (c) => {
const result = await c.env.DB.prepare("SELECT * FROM users").all();
return c.json(result);
});
app.get("/config", (c) => {
return c.json({ apiKey: c.env.API_KEY.slice(0, 4) + "..." });
});
describe("With mocked bindings", () => {
const mockDB = {
prepare: () => ({
all: async () => ({ results: [{ id: 1, name: "Alice" }] }),
first: async () => ({ id: 1, name: "Alice" }),
run: async () => ({ success: true }),
}),
};
const mockKV = {
get: async (key: string) => "cached-value",
put: async (key: string, value: string) => {},
delete: async (key: string) => {},
};
const mockEnv: Bindings = {
DB: mockDB as unknown as D1Database,
KV: mockKV as unknown as KVNamespace,
API_KEY: "test-api-key-12345",
};
it("should use mocked database", async () => {
const res = await app.request("/data", {}, mockEnv);
expect(res.status).toBe(200);
const data = await res.json();
expect(data.results).toHaveLength(1);
});
it("should use mocked API key", async () => {
const res = await app.request("/config", {}, mockEnv);
const data = await res.json();
expect(data.apiKey).toBe("test...");
});
});
Using Miniflare
For more realistic Cloudflare Workers testing:
import { Miniflare } from "miniflare";
import { describe, it, expect, beforeAll, afterAll } from "vitest";
describe("With Miniflare", () => {
let mf: Miniflare;
beforeAll(async () => {
mf = new Miniflare({
script: `
import app from './src/index'
export default app
`,
modules: true,
d1Databases: ["DB"],
kvNamespaces: ["KV"],
});
});
afterAll(async () => {
await mf.dispose();
});
it("should work with real bindings", async () => {
const res = await mf.dispatchFetch("http://localhost/data");
expect(res.status).toBe(200);
});
});
Testing Middleware
import { Hono } from "hono";
import { createMiddleware } from "hono/factory";
const authMiddleware = createMiddleware(async (c, next) => {
const token = c.req.header("Authorization")?.replace("Bearer ", "");
if (!token) {
return c.json({ error: "Unauthorized" }, 401);
}
if (token !== "valid-token") {
return c.json({ error: "Invalid token" }, 403);
}
c.set("userId", "user-123");
await next();
});
const app = new Hono();
app.use("/protected/*", authMiddleware);
app.get("/protected/data", (c) => {
const userId = c.get("userId");
return c.json({ userId, data: "secret" });
});
describe("Auth middleware", () => {
it("should reject request without token", async () => {
const res = await app.request("/protected/data");
expect(res.status).toBe(401);
expect(await res.json()).toEqual({ error: "Unauthorized" });
});
it("should reject invalid token", async () => {
const res = await app.request("/protected/data", {
headers: { Authorization: "Bearer invalid" },
});
expect(res.status).toBe(403);
expect(await res.json()).toEqual({ error: "Invalid token" });
});
it("should allow valid token", async () => {
const res = await app.request("/protected/data", {
headers: { Authorization: "Bearer valid-token" },
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.userId).toBe("user-123");
});
});
Testing Error Handling
import { Hono } from "hono";
import { HTTPException } from "hono/http-exception";
const app = new Hono();
app.get("/error", () => {
throw new HTTPException(500, { message: "Something went wrong" });
});
app.get("/not-found", (c) => {
return c.notFound();
});
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status);
}
return c.json({ error: "Internal error" }, 500);
});
app.notFound((c) => {
return c.json({ error: "Not found" }, 404);
});
describe("Error handling", () => {
it("should handle HTTPException", async () => {
const res = await app.request("/error");
expect(res.status).toBe(500);
expect(await res.json()).toEqual({ error: "Something went wrong" });
});
it("should handle not found", async () => {
const res = await app.request("/unknown-route");
expect(res.status).toBe(404);
expect(await res.json()).toEqual({ error: "Not found" });
});
});
Testing File Uploads
import { Hono } from "hono";
const app = new Hono();
app.post("/upload", async (c) => {
const formData = await c.req.formData();
const file = formData.get("file") as File;
if (!file) {
return c.json({ error: "No file provided" }, 400);
}
return c.json({
filename: file.name,
size: file.size,
type: file.type,
});
});
describe("File upload", () => {
it("should handle file upload", async () => {
const file = new File(["hello world"], "test.txt", { type: "text/plain" });
const formData = new FormData();
formData.append("file", file);
const res = await app.request("/upload", {
method: "POST",
body: formData,
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.filename).toBe("test.txt");
expect(data.size).toBe(11);
expect(data.type).toBe("text/plain");
});
it("should reject missing file", async () => {
const formData = new FormData();
const res = await app.request("/upload", {
method: "POST",
body: formData,
});
expect(res.status).toBe(400);
});
});
Test Setup Patterns
Vitest Configuration
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
coverage: {
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "dist/"],
},
},
});
Test Utilities
import { Hono } from "hono";
import type { Bindings } from "../src/types";
export function createTestApp() {
return new Hono<{ Bindings: Bindings }>();
}
export function createMockEnv(overrides: Partial<Bindings> = {}): Bindings {
return {
DB: createMockDB(),
KV: createMockKV(),
API_KEY: "test-key",
...overrides,
};
}
export function createMockDB() {
return {
prepare: (sql: string) => ({
bind: (...args: any[]) => ({
all: async () => ({ results: [] }),
first: async () => null,
run: async () => ({ success: true }),
}),
all: async () => ({ results: [] }),
first: async () => null,
run: async () => ({ success: true }),
}),
};
}
export function createMockKV() {
const store = new Map<string, string>();
return {
get: async (key: string) => store.get(key) ?? null,
put: async (key: string, value: string) => {
store.set(key, value);
},
delete: async (key: string) => {
store.delete(key);
},
};
}
Using Test Utilities
import { describe, it, expect, beforeEach } from "vitest";
import { createTestApp, createMockEnv } from "./utils";
import { setupRoutes } from "../src/routes";
describe("API Tests", () => {
let app: ReturnType<typeof createTestApp>;
let env: ReturnType<typeof createMockEnv>;
beforeEach(() => {
app = createTestApp();
env = createMockEnv();
setupRoutes(app);
});
it("should work with fresh instances", async () => {
const res = await app.request("/api/health", {}, env);
expect(res.status).toBe(200);
});
});
Quick Reference
app.request() Signature
app.request(
path: string,
options?: RequestInit,
env?: Bindings
): Promise<Response>
Common Assertions
expect(res.status).toBe(200);
expect(res.ok).toBe(true);
expect(res.headers.get("Content-Type")).toContain("application/json");
expect(res.headers.get("X-Custom")).toBe("value");
expect(await res.text()).toBe("Hello");
expect(await res.json()).toEqual({ key: "value" });
expect(res.redirected).toBe(false);
expect(res.url).toBe("http://localhost/path");
Test Client Methods
const client = testClient(app);
client.path.$get();
client.path.$post({ json: {} });
client.path[":id"].$get({ param: { id: "1" } });
client.path.$get({ query: { page: 1 } });
client.path.$post({ header: { "X-Custom": "v" } });
Related Skills
- hono-core - Framework fundamentals
- hono-middleware - Middleware patterns
- hono-validation - Request validation
Version: Hono 4.x
Last Updated: January 2025
License: MIT