Import
import { testClient } from 'hono/testing'
Functions
testClient()
Creates a type-safe test client for making requests to your Hono application.function testClient<T extends Hono<any, Schema, string>>(
app: T,
Env?: ExtractEnv<T>['Bindings'] | {},
executionCtx?: ExecutionContext,
options?: Omit<ClientRequestOptions, 'fetch'>
): UnionToIntersection<Client<T, 'http://localhost'>>
The Hono application instance to test
Optional environment bindings (e.g., database, API keys)
Optional execution context for Cloudflare Workers
Additional client options (headers, etc.)
A fully typed client matching your app’s routes
import { Hono } from 'hono'
import { testClient } from 'hono/testing'
import { describe, it, expect } from 'vitest'
const app = new Hono()
.get('/hello', (c) => c.text('Hello World'))
.post('/posts', async (c) => {
const { title } = await c.req.json()
return c.json({ id: 1, title }, 201)
})
describe('App', () => {
it('GET /hello', async () => {
const client = testClient(app)
const res = await client.hello.$get()
expect(res.status).toBe(200)
expect(await res.text()).toBe('Hello World')
})
it('POST /posts', async () => {
const client = testClient(app)
const res = await client.posts.$post({
json: { title: 'New Post' }
})
expect(res.status).toBe(201)
const data = await res.json()
expect(data.title).toBe('New Post')
})
})
Testing with Environment Bindings
Test applications that use environment variables:import { testClient } from 'hono/testing'
type Env = {
Bindings: {
DB: Database
API_KEY: string
}
}
const app = new Hono<Env>()
app.get('/data', async (c) => {
const data = await c.env.DB.query('SELECT * FROM users')
return c.json(data)
})
describe('App with bindings', () => {
it('should access database', async () => {
const mockDB = {
query: vi.fn().mockResolvedValue([{ id: 1, name: 'Alice' }])
}
const client = testClient(app, {
DB: mockDB,
API_KEY: 'test-key'
})
const res = await client.data.$get()
const data = await res.json()
expect(mockDB.query).toHaveBeenCalled()
expect(data).toEqual([{ id: 1, name: 'Alice' }])
})
})
Testing with Variables
Test applications that use context variables:import { testClient } from 'hono/testing'
type Env = {
Variables: {
user: { id: string; name: string }
}
}
const app = new Hono<Env>()
app.use('*', async (c, next) => {
c.set('user', { id: '1', name: 'Alice' })
await next()
})
app.get('/profile', (c) => {
const user = c.get('user')
return c.json(user)
})
describe('App with variables', () => {
it('should return user profile', async () => {
const client = testClient(app)
const res = await client.profile.$get()
const user = await res.json()
expect(user.name).toBe('Alice')
})
})
Type-Safe Request/Response
The test client provides full type safety:import { testClient } from 'hono/testing'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const schema = z.object({
name: z.string(),
age: z.number()
})
const app = new Hono()
.post('/users', zValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({ id: 1, ...data })
})
describe('Type-safe testing', () => {
it('should validate types', async () => {
const client = testClient(app)
// ✓ TypeScript ensures correct types
const res = await client.users.$post({
json: {
name: 'Alice',
age: 30
}
})
const data = await res.json()
// data is typed as { id: number, name: string, age: number }
expect(data.id).toBe(1)
})
})
Testing Different HTTP Methods
import { testClient } from 'hono/testing'
const app = new Hono()
.get('/posts/:id', (c) => {
return c.json({ id: c.req.param('id'), title: 'Post' })
})
.put('/posts/:id', async (c) => {
const data = await c.req.json()
return c.json({ id: c.req.param('id'), ...data })
})
.delete('/posts/:id', (c) => {
return c.json({ deleted: true })
})
describe('HTTP methods', () => {
const client = testClient(app)
it('GET with params', async () => {
const res = await client.posts[':id'].$get({
param: { id: '123' }
})
const data = await res.json()
expect(data.id).toBe('123')
})
it('PUT with body', async () => {
const res = await client.posts[':id'].$put({
param: { id: '123' },
json: { title: 'Updated' }
})
const data = await res.json()
expect(data.title).toBe('Updated')
})
it('DELETE', async () => {
const res = await client.posts[':id'].$delete({
param: { id: '123' }
})
const data = await res.json()
expect(data.deleted).toBe(true)
})
})
Testing Headers and Cookies
import { testClient } from 'hono/testing'
const app = new Hono()
.get('/protected', (c) => {
const auth = c.req.header('Authorization')
if (!auth) {
return c.text('Unauthorized', 401)
}
return c.json({ message: 'Protected data' })
})
describe('Headers', () => {
it('should send authorization header', async () => {
const client = testClient(app, {}, undefined, {
headers: {
Authorization: 'Bearer token123'
}
})
const res = await client.protected.$get()
expect(res.status).toBe(200)
})
it('should handle missing auth', async () => {
const client = testClient(app)
const res = await client.protected.$get()
expect(res.status).toBe(401)
})
})
Testing with Query Parameters
import { testClient } from 'hono/testing'
const app = new Hono()
.get('/search', (c) => {
const query = c.req.query('q')
const page = c.req.query('page') || '1'
return c.json({ query, page })
})
describe('Query parameters', () => {
it('should handle query params', async () => {
const client = testClient(app)
const res = await client.search.$get({
query: {
q: 'test',
page: '2'
}
})
const data = await res.json()
expect(data.query).toBe('test')
expect(data.page).toBe('2')
})
})
Testing Nested Routes
import { testClient } from 'hono/testing'
const api = new Hono()
.get('/users', (c) => c.json([]))
.get('/posts', (c) => c.json([]))
const app = new Hono()
.route('/api', api)
describe('Nested routes', () => {
it('should access nested routes', async () => {
const client = testClient(app)
const usersRes = await client.api.users.$get()
expect(usersRes.status).toBe(200)
const postsRes = await client.api.posts.$get()
expect(postsRes.status).toBe(200)
})
})
Complete Test Example
import { Hono } from 'hono'
import { testClient } from 'hono/testing'
import { describe, it, expect, beforeEach } from 'vitest'
type Env = {
Bindings: {
DB: Database
}
}
const app = new Hono<Env>()
app.get('/health', (c) => c.json({ status: 'ok' }))
app.post('/users', async (c) => {
const { name, email } = await c.req.json()
const user = await c.env.DB.createUser({ name, email })
return c.json(user, 201)
})
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const user = await c.env.DB.getUser(id)
if (!user) {
return c.notFound()
}
return c.json(user)
})
describe('User API', () => {
let mockDB: any
beforeEach(() => {
mockDB = {
createUser: vi.fn(),
getUser: vi.fn()
}
})
it('should return health status', async () => {
const client = testClient(app, { DB: mockDB })
const res = await client.health.$get()
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ status: 'ok' })
})
it('should create user', async () => {
mockDB.createUser.mockResolvedValue({
id: '1',
name: 'Alice',
email: '[email protected]'
})
const client = testClient(app, { DB: mockDB })
const res = await client.users.$post({
json: {
name: 'Alice',
email: '[email protected]'
}
})
expect(res.status).toBe(201)
expect(mockDB.createUser).toHaveBeenCalledWith({
name: 'Alice',
email: '[email protected]'
})
})
it('should get user by id', async () => {
mockDB.getUser.mockResolvedValue({
id: '1',
name: 'Alice',
email: '[email protected]'
})
const client = testClient(app, { DB: mockDB })
const res = await client.users[':id'].$get({
param: { id: '1' }
})
expect(res.status).toBe(200)
const user = await res.json()
expect(user.id).toBe('1')
})
it('should return 404 for non-existent user', async () => {
mockDB.getUser.mockResolvedValue(null)
const client = testClient(app, { DB: mockDB })
const res = await client.users[':id'].$get({
param: { id: '999' }
})
expect(res.status).toBe(404)
})
})