Skip to main content

supertest vs fastify.inject vs hono/testing: API Integration Testing (2026)

·PkgPulse Team

TL;DR

supertest is the Express-era standard for API integration testing — wraps your Express (or any Node.js HTTP server) and lets you make HTTP assertions against it without starting a real server. fastify.inject is Fastify's built-in request injection — tests Fastify routes in-process with zero overhead, no HTTP port needed. hono/testing is Hono's testing helper — works in any runtime (Node, Bun, Deno, Cloudflare Workers), passes a Request object directly to your app. In 2026: use the framework's native testing approach (fastify.inject for Fastify, hono/testing for Hono) over supertest for faster, more accurate tests.

Key Takeaways

  • supertest: ~5M weekly downloads — Express standard, wraps any http.Server, rich assertion chain
  • fastify.inject: bundled with Fastify — zero-overhead in-process injection, no real HTTP
  • hono/testing: bundled with Hono — framework-agnostic testClient, works in all runtimes
  • supertest starts a real TCP listener (or uses in-memory socket) — slower, closer to production behavior
  • fastify.inject / hono testing bypass the network entirely — pure in-process function calls
  • Both approaches test the full route handler, middleware, and validation pipeline

Why Integration Test APIs?

Unit tests: test individual functions in isolation
Integration tests: test the full request/response pipeline

What integration tests catch that unit tests miss:
  - Route registration bugs (wrong HTTP method, wrong path)
  - Middleware chain errors (auth middleware not applied)
  - Input validation failures (Zod/Fastify schema rejections)
  - Serialization errors (response doesn't match schema)
  - Error handler behavior (500 vs 422 vs 404)
  - Database integration with test data

Goal: test your API as close to how it's actually used as possible,
without spinning up external services or a real HTTP server port.

supertest

supertest — HTTP integration testing:

Basic usage with Express

import request from "supertest"
import { describe, it, expect } from "vitest"
import { app } from "../src/app"  // Express app (not .listen()-ed)

describe("GET /api/packages/:name", () => {
  it("returns package data", async () => {
    const response = await request(app)
      .get("/api/packages/react")
      .set("Authorization", "Bearer test-token")
      .expect(200)
      .expect("Content-Type", /json/)

    expect(response.body.name).toBe("react")
    expect(response.body.healthScore).toBeGreaterThan(0)
  })

  it("returns 404 for unknown package", async () => {
    await request(app)
      .get("/api/packages/nonexistent-package-xyz")
      .expect(404)
  })

  it("returns 401 without auth", async () => {
    await request(app)
      .get("/api/packages/react")
      .expect(401)
  })
})

POST with body

import request from "supertest"
import { app } from "../src/app"
import { db } from "../src/db"

describe("POST /api/packages", () => {
  afterEach(async () => {
    await db.package.deleteMany({ where: { name: "test-package" } })
  })

  it("creates a package", async () => {
    const response = await request(app)
      .post("/api/packages")
      .set("Authorization", "Bearer admin-token")
      .set("Content-Type", "application/json")
      .send({ name: "test-package", version: "1.0.0" })
      .expect(201)

    expect(response.body.id).toBeDefined()
    expect(response.body.name).toBe("test-package")
  })

  it("returns 400 for invalid input", async () => {
    const response = await request(app)
      .post("/api/packages")
      .set("Authorization", "Bearer admin-token")
      .send({ name: "" })  // Invalid: empty name
      .expect(400)

    expect(response.body.errors).toBeDefined()
  })
})

Persistent agent (reuse sessions/cookies)

import request from "supertest"
import { app } from "../src/app"

describe("authenticated user flow", () => {
  const agent = request.agent(app)  // Persists cookies between requests

  it("logs in", async () => {
    await agent
      .post("/auth/login")
      .send({ email: "test@example.com", password: "password" })
      .expect(200)
    // Cookie is now stored in agent
  })

  it("accesses protected route after login", async () => {
    await agent
      .get("/api/profile")
      .expect(200)  // Session cookie sent automatically
  })
})

fastify.inject

Fastify — built-in injection testing:

Basic usage

import Fastify from "fastify"
import { describe, it, expect, beforeAll, afterAll } from "vitest"
import { packageRoutes } from "../src/routes/packages"

let fastify: ReturnType<typeof Fastify>

beforeAll(async () => {
  fastify = Fastify({ logger: false })
  await fastify.register(packageRoutes, { prefix: "/api" })
  await fastify.ready()
})

afterAll(async () => {
  await fastify.close()
})

describe("GET /api/packages/:name", () => {
  it("returns package data", async () => {
    const response = await fastify.inject({
      method: "GET",
      url: "/api/packages/react",
      headers: {
        Authorization: "Bearer test-token",
      },
    })

    expect(response.statusCode).toBe(200)
    expect(response.json().name).toBe("react")
  })

  it("validates route parameters", async () => {
    const response = await fastify.inject({
      method: "GET",
      url: "/api/packages/",  // Missing name
    })

    expect(response.statusCode).toBe(404)
  })
})

POST with body

describe("POST /api/packages", () => {
  it("creates a package with valid schema", async () => {
    const response = await fastify.inject({
      method: "POST",
      url: "/api/packages",
      headers: {
        "Content-Type": "application/json",
        Authorization: "Bearer admin-token",
      },
      payload: {
        name: "my-package",
        version: "1.0.0",
        description: "A test package",
      },
    })

    expect(response.statusCode).toBe(201)
    const body = response.json()
    expect(body.id).toBeDefined()
  })

  it("rejects invalid schema", async () => {
    const response = await fastify.inject({
      method: "POST",
      url: "/api/packages",
      payload: { name: 123 },  // name should be a string
    })

    // Fastify's built-in schema validation returns 400:
    expect(response.statusCode).toBe(400)
    expect(response.json().message).toMatch(/body\/name/)
  })
})
// src/app.ts — export a factory function:
import Fastify, { FastifyInstance } from "fastify"

export async function buildApp(opts = {}): Promise<FastifyInstance> {
  const app = Fastify({ logger: false, ...opts })
  await app.register(import("./plugins/auth"))
  await app.register(import("./routes/packages"), { prefix: "/api" })
  return app
}

// test/packages.test.ts:
import { buildApp } from "../src/app"

describe("Package API", () => {
  let app: Awaited<ReturnType<typeof buildApp>>

  beforeAll(async () => {
    app = await buildApp()
  })

  afterAll(async () => {
    await app.close()
  })

  it("responds to health check", async () => {
    const res = await app.inject({ method: "GET", url: "/api/health" })
    expect(res.statusCode).toBe(200)
  })
})

hono/testing

Hono — testClient for all runtimes:

Basic testing

import { describe, it, expect } from "vitest"
import { testClient } from "hono/testing"
import { app } from "../src/app"  // Hono app

describe("Hono API", () => {
  const client = testClient(app)

  it("GET /api/packages/:name", async () => {
    const response = await client.api.packages[":name"].$get({
      param: { name: "react" },
      header: { Authorization: "Bearer test-token" },
    })

    expect(response.status).toBe(200)
    const data = await response.json()
    expect(data.name).toBe("react")
  })

  it("POST /api/packages", async () => {
    const response = await client.api.packages.$post({
      json: { name: "my-package", version: "1.0.0" },
      header: { Authorization: "Bearer admin-token" },
    })

    expect(response.status).toBe(201)
  })
})

Direct fetch approach (works without testClient)

import { app } from "../src/app"

describe("Hono fetch testing", () => {
  it("returns package data", async () => {
    const req = new Request("http://localhost/api/packages/react", {
      headers: { Authorization: "Bearer test-token" },
    })

    const res = await app.fetch(req)

    expect(res.status).toBe(200)
    const data = await res.json()
    expect(data.name).toBe("react")
  })
})

Works in all runtimes

// Same test file works in Node.js (Vitest), Bun, Deno:
// Hono's testClient passes Request objects directly to the app
// No TCP socket, no HTTP server, no runtime-specific code

// For Cloudflare Workers tests (Wrangler + Miniflare):
const env = getMiniflareBindings()
const res = await app.fetch(req, env)

Feature Comparison

Featuresupertestfastify.injecthono/testing
FrameworkAny Node.js serverFastify onlyHono only
TransportIn-memory HTTPPure in-processPure in-process
Runtime supportNode.jsNode.jsAny (Node, Bun, Deno, CF Workers)
Cookie handling✅ (agent)Manual
Assertion chain✅ (fluent)❌ (manual)❌ (manual)
TypeScript types✅ (RPC-style)
Weekly downloads~5Mbundled w/ Fastifybundled w/ Hono

When to Use Each

Use supertest if:

  • Testing Express, Koa, or any Node.js HTTP server
  • Team is already familiar with supertest's fluent assertion chain
  • Need cookie/session testing with request.agent()
  • Migrating existing Express tests

Use fastify.inject if:

  • Building a Fastify application — it's zero-dependency, built in
  • Testing Fastify's schema validation behavior
  • Need full plugin/decorator lifecycle in tests

Use hono/testing (testClient) if:

  • Building a Hono application
  • Need tests that run in multiple runtimes (Node, Bun, Cloudflare Workers)
  • Want type-safe route testing with RPC-style client

Tip: Always test the real app, not mocks

// ❌ Too isolated — misses middleware, validation, error handlers:
const handler = require("./routes/packages")
const result = handler({ params: { name: "react" } }, mockRes)

// ✅ Tests the full stack:
const response = await app.inject({ method: "GET", url: "/api/packages/react" })

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on supertest v7.x, fastify v5.x, and hono v4.x.

Compare testing and API framework packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.