Skip to main content

Overview

Testing is crucial for building reliable applications. Hono provides excellent testing utilities that make it easy to test your routes, middleware, and handlers.

Test Client

The testClient helper allows you to test your Hono app with full type safety:
import { Hono } from 'hono'
import { testClient } from 'hono/testing'

const app = new Hono()
  .get('/posts/:id', (c) => {
    const id = c.req.param('id')
    return c.json({ id, title: 'Hello' })
  })
  .post('/posts', async (c) => {
    const body = await c.req.json()
    return c.json({ id: '123', ...body }, 201)
  })

// In your test file
const client = testClient(app)

const res = await client.posts[':id'].$get({
  param: { id: '123' }
})

expect(res.status).toBe(200)

const data = await res.json()
expect(data).toEqual({ id: '123', title: 'Hello' })

Using app.request()

You can also test directly using app.request():
import { Hono } from 'hono'

const app = new Hono()

app.get('/hello', (c) => c.text('Hello Hono!'))

test('GET /hello', async () => {
  const res = await app.request('http://localhost/hello')
  expect(res.status).toBe(200)
  expect(await res.text()).toBe('Hello Hono!')
})

Testing with Vitest

Hono works great with Vitest:
import { describe, expect, it } from 'vitest'
import { Hono } from 'hono'
import { testClient } from 'hono/testing'

const app = new Hono()
  .get('/users/:id', (c) => {
    return c.json({ id: c.req.param('id') })
  })

describe('User API', () => {
  const client = testClient(app)

  it('GET /users/:id - should return user', async () => {
    const res = await client.users[':id'].$get({
      param: { id: '123' }
    })

    expect(res.status).toBe(200)

    const data = await res.json()
    expect(data).toEqual({ id: '123' })
  })
})

Testing with Jest

Hono also works with Jest:
import { Hono } from 'hono'

const app = new Hono()

app.post('/posts', async (c) => {
  const body = await c.req.json()
  return c.json({ id: '1', ...body }, 201)
})

describe('POST /posts', () => {
  it('should create a post', async () => {
    const res = await app.request('http://localhost/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        title: 'Hello',
        body: 'World'
      }),
    })

    expect(res.status).toBe(201)

    const data = await res.json()
    expect(data).toMatchObject({
      id: '1',
      title: 'Hello',
      body: 'World',
    })
  })
})

Testing Middleware

Test custom middleware:
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'

const authMiddleware: MiddlewareHandler = async (c, next) => {
  const token = c.req.header('Authorization')
  if (!token) {
    return c.text('Unauthorized', 401)
  }
  await next()
}

const app = new Hono()
app.use('/admin/*', authMiddleware)
app.get('/admin/dashboard', (c) => c.text('Dashboard'))

test('should require authentication', async () => {
  const res = await app.request('http://localhost/admin/dashboard')
  expect(res.status).toBe(401)
})

test('should allow authenticated requests', async () => {
  const res = await app.request('http://localhost/admin/dashboard', {
    headers: {
      Authorization: 'Bearer token123',
    },
  })
  expect(res.status).toBe(200)
})

Testing with Environment Variables

Test with bindings (environment variables):
import { testClient } from 'hono/testing'

type Env = {
  Bindings: {
    API_KEY: string
    DATABASE_URL: string
  }
}

const app = new Hono<Env>()

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

test('should access environment variables', async () => {
  const mockEnv = {
    API_KEY: 'test-key',
    DATABASE_URL: 'test-db-url',
  }

  const client = testClient(app, mockEnv)

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

  expect(data).toEqual({
    apiKey: 'test-key',
    dbUrl: 'test-db-url',
  })
})

Testing Validation

Test routes with validation:
import { Hono } from 'hono'
import { validator } from 'hono/validator'

const app = new Hono()

app.post(
  '/users',
  validator('json', (value, c) => {
    const data = value as { email: string }
    if (!data.email || !data.email.includes('@')) {
      return c.text('Invalid email', 400)
    }
    return data
  }),
  (c) => {
    const data = c.req.valid('json')
    return c.json({ created: true, email: data.email })
  }
)

test('should validate email', async () => {
  const res = await app.request('http://localhost/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email: 'invalid' }),
  })

  expect(res.status).toBe(400)
  expect(await res.text()).toBe('Invalid email')
})

test('should accept valid email', async () => {
  const res = await app.request('http://localhost/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email: '[email protected]' }),
  })

  expect(res.status).toBe(200)
})

Testing Error Handling

Test error handlers:
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'

const app = new Hono()

app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json({ error: err.message }, err.status)
  }
  return c.json({ error: 'Internal Server Error' }, 500)
})

app.get('/error', () => {
  throw new HTTPException(400, { message: 'Bad Request' })
})

test('should handle errors', async () => {
  const res = await app.request('http://localhost/error')
  expect(res.status).toBe(400)

  const data = await res.json()
  expect(data).toEqual({ error: 'Bad Request' })
})

Testing Headers and Cookies

Test requests with headers and cookies:
import { Hono } from 'hono'
import { getCookie, setCookie } from 'hono/cookie'

const app = new Hono()

app.get('/cookie', (c) => {
  const value = getCookie(c, 'session')
  return c.text(`Cookie: ${value}`)
})

app.post('/set-cookie', (c) => {
  setCookie(c, 'session', 'abc123')
  return c.text('Cookie set')
})

test('should read cookies', async () => {
  const res = await app.request('http://localhost/cookie', {
    headers: {
      Cookie: 'session=test123',
    },
  })

  expect(await res.text()).toBe('Cookie: test123')
})

test('should set cookies', async () => {
  const res = await app.request('http://localhost/set-cookie', {
    method: 'POST',
  })

  const setCookieHeader = res.headers.get('Set-Cookie')
  expect(setCookieHeader).toContain('session=abc123')
})

Mocking External Services

Mock external API calls:
import { vi } from 'vitest'
import { Hono } from 'hono'

const fetchUser = async (id: string) => {
  const res = await fetch(`https://api.example.com/users/${id}`)
  return res.json()
}

const app = new Hono()

app.get('/users/:id', async (c) => {
  const user = await fetchUser(c.req.param('id'))
  return c.json(user)
})

test('should fetch user data', async () => {
  // Mock the fetch function
  global.fetch = vi.fn().mockResolvedValue({
    json: async () => ({ id: '123', name: 'John' }),
  })

  const res = await app.request('http://localhost/users/123')
  const data = await res.json()

  expect(data).toEqual({ id: '123', name: 'John' })
  expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/123')
})

Best Practices

The testClient helper provides full type safety and better developer experience.
Test error conditions, invalid input, and edge cases, not just happy paths.
Always mock external APIs and databases in unit tests.
Create focused tests for middleware before testing full request flows.
Write clear test descriptions that explain what is being tested.

Build docs developers (and LLMs) love