Skip to main content
The Testing helper provides utilities for testing your Hono applications with type-safe request methods.

Import

import { testClient } from 'hono/testing'

Basic Usage

Create a type-safe client for testing your app:
import { Hono } from 'hono'
import { testClient } from 'hono/testing'

const app = new Hono()
  .get('/posts', (c) => c.json({ posts: [] }))
  .post('/posts', (c) => c.json({ id: 1 }, 201))

const client = testClient(app)

// Make requests
const res = await client.posts.$get()
expect(res.status).toBe(200)

const data = await res.json()
expect(data).toEqual({ posts: [] })

Features

Type-Safe Requests

The test client provides full TypeScript support:
import { Hono } from 'hono'
import { testClient } from 'hono/testing'

const app = new Hono()
  .get('/users/:id', (c) => {
    const id = c.req.param('id')
    return c.json({ id, name: 'John' })
  })
  .post('/users', async (c) => {
    const body = await c.req.json()
    return c.json({ id: 1, ...body }, 201)
  })

const client = testClient(app)

// Type-safe requests
const getRes = await client.users[':id'].$get({
  param: { id: '123' }
})

const postRes = await client.users.$post({
  json: { name: 'Jane' }
})

Query Parameters

Test routes with query parameters:
import { Hono } from 'hono'
import { testClient } from 'hono/testing'

const app = new Hono()
  .get('/search', (c) => {
    const query = c.req.query('q')
    return c.json({ query })
  })

const client = testClient(app)

const res = await client.search.$get({
  query: { q: 'hono' }
})

const data = await res.json()
expect(data.query).toBe('hono')

Request Headers

Include custom headers in requests:
import { Hono } from 'hono'
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({ data: 'secret' })
  })

const client = testClient(app)

const res = await client.protected.$get({
  headers: {
    Authorization: 'Bearer token123'
  }
})

expect(res.status).toBe(200)

JSON Request Body

Send JSON data in requests:
import { Hono } from 'hono'
import { testClient } from 'hono/testing'

const app = new Hono()
  .post('/api/data', async (c) => {
    const body = await c.req.json<{ name: string; age: number }>()
    return c.json({ received: body })
  })

const client = testClient(app)

const res = await client.api.data.$post({
  json: { name: 'Alice', age: 30 }
})

const data = await res.json()
expect(data.received.name).toBe('Alice')

Form Data

Test file uploads and form submissions:
import { Hono } from 'hono'
import { testClient } from 'hono/testing'

const app = new Hono()
  .post('/upload', async (c) => {
    const body = await c.req.parseBody()
    return c.json({ filename: body.file?.name })
  })

const client = testClient(app)

const formData = new FormData()
formData.append('file', new Blob(['content']), 'test.txt')

const res = await client.upload.$post({
  body: formData
})

Environment Variables

Test with environment bindings:
import { Hono } from 'hono'
import { testClient } from 'hono/testing'

type Bindings = {
  API_KEY: string
  DATABASE_URL: string
}

const app = new Hono<{ Bindings: Bindings }>()
  .get('/config', (c) => {
    return c.json({
      apiKey: c.env.API_KEY,
      dbUrl: c.env.DATABASE_URL
    })
  })

const client = testClient(app, {
  API_KEY: 'test-key',
  DATABASE_URL: 'postgres://localhost'
})

const res = await client.config.$get()
const data = await res.json()

expect(data.apiKey).toBe('test-key')

Execution Context

Provide execution context for testing:
import { testClient } from 'hono/testing'
import type { ExecutionContext } from 'hono'

const mockContext: ExecutionContext = {
  waitUntil: (promise: Promise<any>) => {},
  passThroughOnException: () => {},
}

const client = testClient(
  app,
  undefined, // Bindings
  mockContext
)

Client Options

Customize the test client:
import { testClient } from 'hono/testing'

const client = testClient(app, undefined, undefined, {
  headers: {
    'User-Agent': 'test-client',
    'Accept-Language': 'en-US'
  }
})

// All requests will include these headers
const res = await client.api.$get()

Testing Patterns

Unit Tests

Test individual routes:
import { describe, it, expect } from 'vitest'
import { testClient } from 'hono/testing'

describe('API Routes', () => {
  it('should return user list', async () => {
    const client = testClient(app)
    const res = await client.users.$get()
    
    expect(res.status).toBe(200)
    const data = await res.json()
    expect(Array.isArray(data.users)).toBe(true)
  })
  
  it('should create a user', async () => {
    const client = testClient(app)
    const res = await client.users.$post({
      json: { name: 'Test User' }
    })
    
    expect(res.status).toBe(201)
    const data = await res.json()
    expect(data.name).toBe('Test User')
  })
})

Integration Tests

Test complete workflows:
import { describe, it, expect } from 'vitest'
import { testClient } from 'hono/testing'

describe('User workflow', () => {
  it('should create and retrieve user', async () => {
    const client = testClient(app)
    
    // Create user
    const createRes = await client.users.$post({
      json: { name: 'Alice', email: '[email protected]' }
    })
    
    expect(createRes.status).toBe(201)
    const created = await createRes.json()
    
    // Retrieve user
    const getRes = await client.users[':id'].$get({
      param: { id: created.id }
    })
    
    expect(getRes.status).toBe(200)
    const retrieved = await getRes.json()
    expect(retrieved.name).toBe('Alice')
  })
})

Testing Middleware

Test middleware behavior:
import { Hono } from 'hono'
import { testClient } from 'hono/testing'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'

const app = new Hono()
  .use('*', logger())
  .use('*', cors())
  .get('/api', (c) => c.json({ ok: true }))

const client = testClient(app)

const res = await client.api.$get()
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*')

Testing Error Cases

Test error handling:
import { Hono } from 'hono'
import { testClient } from 'hono/testing'

const app = new Hono()
  .get('/error', (c) => {
    throw new Error('Something went wrong')
  })
  .onError((err, c) => {
    return c.json({ error: err.message }, 500)
  })

const client = testClient(app)

const res = await client.error.$get()
expect(res.status).toBe(500)

const data = await res.json()
expect(data.error).toBe('Something went wrong')

Testing with Different Test Frameworks

Vitest

import { describe, it, expect, beforeEach } from 'vitest'
import { testClient } from 'hono/testing'
import { app } from './app'

describe('API', () => {
  let client: ReturnType<typeof testClient>
  
  beforeEach(() => {
    client = testClient(app)
  })
  
  it('works', async () => {
    const res = await client.index.$get()
    expect(res.status).toBe(200)
  })
})

Jest

import { testClient } from 'hono/testing'
import { app } from './app'

describe('API', () => {
  let client: ReturnType<typeof testClient>
  
  beforeEach(() => {
    client = testClient(app)
  })
  
  test('returns 200', async () => {
    const res = await client.index.$get()
    expect(res.status).toBe(200)
  })
})

Node Test Runner

import { describe, it } from 'node:test'
import assert from 'node:assert'
import { testClient } from 'hono/testing'
import { app } from './app'

describe('API', () => {
  it('returns 200', async () => {
    const client = testClient(app)
    const res = await client.index.$get()
    assert.strictEqual(res.status, 200)
  })
})

URL Generation

Generate URLs for routes:
import { testClient } from 'hono/testing'

const client = testClient(app)

const url = client.users[':id'].$url({
  param: { id: '123' },
  query: { include: 'posts' }
})

console.log(url.pathname) // /users/123
console.log(url.searchParams.get('include')) // posts

Mock Responses

Mock external dependencies:
import { Hono } from 'hono'
import { testClient } from 'hono/testing'

const app = new Hono()
  .get('/external', async (c) => {
    const res = await fetch('https://api.example.com/data')
    const data = await res.json()
    return c.json(data)
  })

// Mock fetch
globalThis.fetch = async () => {
  return new Response(JSON.stringify({ mocked: true }))
}

const client = testClient(app)
const res = await client.external.$get()
const data = await res.json()

expect(data.mocked).toBe(true)

Best Practices

Type Safety

Leverage TypeScript for compile-time type checking of requests

Isolate Tests

Create a new test client for each test to avoid side effects

Test Real Paths

Test actual user paths through your application

Mock External APIs

Mock external dependencies for faster, reliable tests
The test client uses the same Hono client (hc) used in production, ensuring your tests reflect real usage.
Test clients run against your Hono app directly without HTTP overhead, making tests fast and reliable.

Build docs developers (and LLMs) love