Deno Integration Testing: HTTP Routes and Database Calls (2026)

Deno Integration Testing: HTTP Routes and Database Calls (2026)

Integration tests verify that your Deno application's components work together — that HTTP routes return the right responses, that database queries return the right data, that error handling works end-to-end. They bridge the gap between unit tests (which verify logic in isolation) and E2E tests (which run a real browser).

This guide covers Deno integration testing patterns for HTTP servers and database layers.

Testing Hono HTTP Routes

Hono is the most popular HTTP framework for Deno in 2026. It provides a testClient utility that lets you test routes without starting a real server.

// app.ts
import { Hono } from "jsr:@hono/hono";
import { db } from "./db.ts";

const app = new Hono();

app.get("/posts", async (c) => {
  const posts = await db.query("SELECT id, title, slug FROM posts ORDER BY created_at DESC");
  return c.json({ posts });
});

app.get("/posts/:slug", async (c) => {
  const slug = c.req.param("slug");
  const rows = await db.query("SELECT * FROM posts WHERE slug = $1", [slug]);

  if (rows.length === 0) {
    return c.json({ error: "Post not found" }, 404);
  }

  return c.json({ post: rows[0] });
});

app.post("/posts", async (c) => {
  const body = await c.req.json();

  if (!body.title?.trim()) {
    return c.json({ error: "Title is required" }, 422);
  }

  if (!body.content?.trim()) {
    return c.json({ error: "Content is required" }, 422);
  }

  const slug = body.title.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");

  const rows = await db.query(
    "INSERT INTO posts (title, content, slug) VALUES ($1, $2, $3) RETURNING id, slug",
    [body.title.trim(), body.content.trim(), slug]
  );

  return c.json({ post: rows[0] }, 201);
});

export default app;
// app_test.ts
import { assertEquals, assertObjectMatch } from "jsr:@std/assert";
import { stub } from "jsr:@std/mock";
import { testClient } from "jsr:@hono/hono/testing";
import app from "./app.ts";
import { db } from "./db.ts";

Deno.test({
  name: "GET /posts returns list of posts",
  permissions: { net: true },
  async fn() {
    const queryStub = stub(db, "query", async () => [
      { id: 1, title: "First Post", slug: "first-post" },
      { id: 2, title: "Second Post", slug: "second-post" },
    ]);

    try {
      const client = testClient(app);
      const res = await client.posts.$get();

      assertEquals(res.status, 200);
      const data = await res.json();
      assertEquals(data.posts.length, 2);
      assertEquals(data.posts[0].title, "First Post");
    } finally {
      queryStub.restore();
    }
  },
});

Deno.test("GET /posts/:slug returns post data", async () => {
  const queryStub = stub(db, "query", async () => [
    { id: 1, title: "Hello World", slug: "hello-world", content: "Post content" },
  ]);

  try {
    const res = await app.request("/posts/hello-world");
    assertEquals(res.status, 200);

    const data = await res.json();
    assertEquals(data.post.title, "Hello World");
  } finally {
    queryStub.restore();
  }
});

Deno.test("GET /posts/:slug returns 404 for unknown slug", async () => {
  const queryStub = stub(db, "query", async () => []);

  try {
    const res = await app.request("/posts/nonexistent-post");
    assertEquals(res.status, 404);
  } finally {
    queryStub.restore();
  }
});

Deno.test("POST /posts creates post and returns 201", async () => {
  const queryStub = stub(db, "query", async () => [
    { id: 3, slug: "my-new-post" },
  ]);

  try {
    const res = await app.request("/posts", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        title: "My New Post",
        content: "This is the post content.",
      }),
    });

    assertEquals(res.status, 201);
    const data = await res.json();
    assertEquals(data.post.slug, "my-new-post");
  } finally {
    queryStub.restore();
  }
});

Deno.test("POST /posts returns 422 when title is missing", async () => {
  const res = await app.request("/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ content: "Content without a title" }),
  });

  assertEquals(res.status, 422);
  const data = await res.json();
  assertEquals(data.error, "Title is required");
});

Testing with the Native Deno HTTP Server

For apps using Deno's built-in Deno.serve, test handlers directly:

// server.ts
export async function handleRequest(req: Request): Promise<Response> {
  const url = new URL(req.url);

  if (url.pathname === "/health") {
    return Response.json({ status: "ok", timestamp: Date.now() });
  }

  if (url.pathname === "/echo" && req.method === "POST") {
    const body = await req.json();
    return Response.json({ received: body });
  }

  return new Response("Not Found", { status: 404 });
}
// server_test.ts
import { assertEquals, assertObjectMatch } from "jsr:@std/assert";
import { handleRequest } from "./server.ts";

Deno.test("GET /health returns status ok", async () => {
  const req = new Request("http://localhost/health");
  const res = await handleRequest(req);

  assertEquals(res.status, 200);
  const data = await res.json();
  assertEquals(data.status, "ok");
});

Deno.test("POST /echo returns received body", async () => {
  const req = new Request("http://localhost/echo", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message: "hello" }),
  });

  const res = await handleRequest(req);
  const data = await res.json();

  assertObjectMatch(data, { received: { message: "hello" } });
});

Deno.test("unknown routes return 404", async () => {
  const req = new Request("http://localhost/unknown-path");
  const res = await handleRequest(req);

  assertEquals(res.status, 404);
});

Integration Tests with a Real Database

For tests that verify real query behavior — correct SQL, constraint enforcement, and transaction handling — use a test database.

SQLite (for local tests)

// db_test.ts
import { assertEquals, assertExists } from "jsr:@std/assert";
import { DB } from "https://deno.land/x/sqlite/mod.ts";

function createTestDb() {
  const db = new DB(":memory:");

  db.execute(`
    CREATE TABLE posts (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      title TEXT NOT NULL,
      slug TEXT NOT NULL UNIQUE,
      content TEXT NOT NULL,
      created_at TEXT DEFAULT CURRENT_TIMESTAMP
    )
  `);

  return db;
}

Deno.test("PostRepository inserts and retrieves a post", () => {
  const db = createTestDb();

  try {
    db.execute(
      "INSERT INTO posts (title, slug, content) VALUES (?, ?, ?)",
      ["Hello World", "hello-world", "Post content"]
    );

    const rows = db.query("SELECT * FROM posts WHERE slug = ?", ["hello-world"]);
    assertEquals(rows.length, 1);
  } finally {
    db.close();
  }
});

Deno.test("PostRepository enforces unique slug constraint", () => {
  const db = createTestDb();

  try {
    db.execute(
      "INSERT INTO posts (title, slug, content) VALUES (?, ?, ?)",
      ["First Post", "my-slug", "Content 1"]
    );

    let threw = false;
    try {
      db.execute(
        "INSERT INTO posts (title, slug, content) VALUES (?, ?, ?)",
        ["Second Post", "my-slug", "Content 2"] // duplicate slug
      );
    } catch {
      threw = true;
    }

    assertEquals(threw, true);
  } finally {
    db.close();
  }
});

PostgreSQL Test Database

For production-grade PostgreSQL tests:

// test_helpers.ts
import { Pool } from "https://deno.land/x/postgres/mod.ts";

const TEST_DATABASE_URL = Deno.env.get("TEST_DATABASE_URL") ??
  "postgres://localhost/myapp_test";

export async function createTestPool(): Promise<Pool> {
  const pool = new Pool(TEST_DATABASE_URL, 1);
  return pool;
}

export async function clearTestData(pool: Pool): Promise<void> {
  const client = await pool.connect();
  try {
    await client.queryArray("TRUNCATE TABLE posts, users RESTART IDENTITY CASCADE");
  } finally {
    client.release();
  }
}
// posts_repository_test.ts
import { assertEquals } from "jsr:@std/assert";
import { createTestPool, clearTestData } from "./test_helpers.ts";
import { PostRepository } from "./posts_repository.ts";

const pool = await createTestPool();
const repo = new PostRepository(pool);

// Use beforeEach equivalent with a test fixture
async function withCleanData(fn: () => Promise<void>) {
  await clearTestData(pool);
  await fn();
}

Deno.test("creates a post and retrieves it by slug", async () => {
  await withCleanData(async () => {
    await repo.create({
      title: "Hello World",
      content: "Post content here",
      slug: "hello-world",
    });

    const post = await repo.findBySlug("hello-world");
    assertEquals(post?.title, "Hello World");
  });
});

Deno.test("returns null for unknown slug", async () => {
  await withCleanData(async () => {
    const post = await repo.findBySlug("does-not-exist");
    assertEquals(post, null);
  });
});

Deno.test("lists posts in reverse chronological order", async () => {
  await withCleanData(async () => {
    await repo.create({ title: "First", slug: "first", content: "Content" });
    await repo.create({ title: "Second", slug: "second", content: "Content" });
    await repo.create({ title: "Third", slug: "third", content: "Content" });

    const posts = await repo.findAll({ limit: 10 });
    assertEquals(posts[0].title, "Third");
    assertEquals(posts[2].title, "First");
  });
});

Run with the test database:

TEST_DATABASE_URL=postgres://localhost/myapp_test deno test --allow-env --allow-net

Testing Middleware

Middleware in Deno HTTP frameworks needs testing when it handles authentication, logging, or request transformation.

// auth_middleware_test.ts
import { assertEquals } from "jsr:@std/assert";
import { Hono } from "jsr:@hono/hono";
import { authMiddleware } from "./auth_middleware.ts";
import { stub } from "jsr:@std/mock";
import * as sessionModule from "./session.ts";

const app = new Hono();
app.use("/protected/*", authMiddleware);
app.get("/protected/data", (c) => c.json({ secret: "value" }));

Deno.test("authMiddleware blocks requests without token", async () => {
  const res = await app.request("/protected/data");
  assertEquals(res.status, 401);
});

Deno.test("authMiddleware allows requests with valid token", async () => {
  const validateStub = stub(
    sessionModule,
    "validateToken",
    async () => ({ userId: "user-1" })
  );

  try {
    const res = await app.request("/protected/data", {
      headers: { Authorization: "Bearer valid-token" },
    });

    assertEquals(res.status, 200);
    const data = await res.json();
    assertEquals(data.secret, "value");
  } finally {
    validateStub.restore();
  }
});

Deno.test("authMiddleware blocks requests with expired token", async () => {
  const validateStub = stub(
    sessionModule,
    "validateToken",
    async () => { throw new Error("Token expired"); }
  );

  try {
    const res = await app.request("/protected/data", {
      headers: { Authorization: "Bearer expired-token" },
    });

    assertEquals(res.status, 401);
  } finally {
    validateStub.restore();
  }
});

CI Configuration

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: myapp_test
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4
      
      - uses: denoland/setup-deno@v2
        with:
          deno-version: v2.x

      - name: Run unit tests
        run: deno test --allow-env src/**/*_test.ts

      - name: Run integration tests
        run: deno test --allow-env --allow-net tests/integration/
        env:
          TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/myapp_test

Production Monitoring with HelpMeTest

Integration tests verify your server and database work correctly together in test conditions. After deployment:

  • Production database might have different indexes or table structures
  • Connection pool limits may cause intermittent failures under load
  • Environment-specific configuration may produce different behavior

HelpMeTest monitors your live Deno API endpoints:

Go to https://mydenoapp.com/posts
Verify the response contains a posts array
Verify the first post has a title

Free tier: 10 tests, 5-minute check intervals.
Pro: $100/month
— unlimited tests, 24/7 monitoring.


Start free at helpmetest.com — no credit card required.

Read more