Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Tests with app.request()

A test is code that sends a request to the app itself and checks the response. In Hono this needs neither a server nor a port: the app.request() method passes a request straight into the app and returns a Response. Tests are run by Vitest, a popular test runner for TypeScript.

The app apart from the server

If a test imports src/index.ts, the server starts along with it. So the app is moved into its own file. Create src/app.ts and move everything except serve() there:

// src/app.ts
import { Hono } from 'hono'
import posts from './routes/posts.js'
import auth from './routes/auth.js'
 
const app = new Hono()
 
app.route('/posts', posts)
app.route('/auth', auth)
 
export default app

Only the start remains in src/index.ts:

// src/index.ts
import { serve } from '@hono/node-server'
import app from './app.js'
 
serve({ fetch: app.fetch, port: Number(process.env.PORT ?? 3000) })

The server works as before, and src/app.ts can be imported in tests.

Installing Vitest

npm install -D vitest

Create vitest.config.ts in the project root with environment variables for tests:

// vitest.config.ts
import { defineConfig } from 'vitest/config'
 
export default defineConfig({
  test: {
    env: {
      DB_FILE: ':memory:',
      JWT_SECRET: 'test-secret',
    },
  },
})

The value :memory: creates an in-memory SQLite database: it is empty on every test run and does not touch blog.db.

The first test

Create src/app.test.ts:

// src/app.test.ts
import { describe, expect, it } from 'vitest'
import app from './app.js'
 
describe('posts', () => {
  it('returns the list', async () => {
    const res = await app.request('/posts')
    expect(res.status).toBe(200)
    expect(await res.json()).toEqual([])
  })
 
  it('returns 404 for a missing post', async () => {
    const res = await app.request('/posts/999')
    expect(res.status).toBe(404)
  })
})

Run the tests:

npx vitest

The terminal shows two green items and the summary 2 passed. Vitest keeps watching the files and reruns the tests on changes.

A request with a body and headers

The second argument of app.request() takes the same options as fetch():

// src/app.test.ts
it('does not create a post without a token', async () => {
  const res = await app.request('/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title: 'Test', body: 'Text' }),
  })
  expect(res.status).toBe(401)
})

The test checks that the requireAuth middleware rejects a request without a token.

The test script

Add the line "test": "vitest run" to the scripts section of package.json. npm test runs the tests once and exits — that is how they are run before deployment.

Common mistake

Tests use the same blog.db database as the development server. Tests create and delete posts, a day later the "list is empty" check stops passing, and your data gets mangled. Always give tests a separate database, like :memory: in vitest.config.ts.

Thanks for reading!

Donate to the site — we work hard for you.