Skip to main content
The Testing Helper provides utilities for testing Hono applications with full type safety and a convenient API client.

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'>>
app
Hono
required
The Hono application instance to test
Env
Bindings
Optional environment bindings (e.g., database, API keys)
executionCtx
ExecutionContext
Optional execution context for Cloudflare Workers
options
ClientRequestOptions
Additional client options (headers, etc.)
return
Client
A fully typed client matching your app’s routes
Example
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)
  })
})

Build docs developers (and LLMs) love