Deno Testing Guide: Built-in Test Runner and Assertions (2026)

Deno Testing Guide: Built-in Test Runner and Assertions (2026)

Deno ships with a built-in test runner. No configuration, no separate test framework, no package.json scripts to set up. Run deno test and you're testing.

This guide covers everything in Deno's testing toolkit: Deno.test, the standard assertion library, subtests, test filtering, code coverage, and patterns for writing maintainable tests.

Running Tests

Write a test file:

// math_test.ts
import { assertEquals } from "jsr:@std/assert";

Deno.test("adds two numbers", () => {
  assertEquals(1 + 2, 3);
});

Run it:

deno test math_test.ts

<span class="hljs-comment"># Run all tests in current directory
deno <span class="hljs-built_in">test

<span class="hljs-comment"># Run with file watching
deno <span class="hljs-built_in">test --watch

<span class="hljs-comment"># Run with coverage
deno <span class="hljs-built_in">test --coverage=cov_profile
deno coverage cov_profile

Deno discovers test files matching *_test.ts, *.test.ts, *_test.js, and *.test.js patterns. The --watch flag re-runs tests on file changes.

Assertions from @std/assert

Deno's standard library provides a comprehensive set of assertions. Import from jsr:@std/assert:

import {
  assertEquals,
  assertNotEquals,
  assertStrictEquals,
  assertThrows,
  assertRejects,
  assertExists,
  assertArrayIncludes,
  assertObjectMatch,
  assertMatch,
  assertGreater,
  assertLess,
  fail,
} from "jsr:@std/assert";

Core Assertions

// Equality
assertEquals("hello", "hello");          // deep equality
assertNotEquals("hello", "world");
assertStrictEquals(1, 1);               // strict (===) equality

// Existence
assertExists("value");                  // not null, not undefined
assertExists({ key: "value" });

// Arrays
assertArrayIncludes([1, 2, 3], [1, 3]); // array includes all expected items

// Objects
assertObjectMatch(
  { name: "Ada", role: "admin", active: true },
  { name: "Ada", role: "admin" }          // matches subset of properties
);

// Strings
assertMatch("hello world", /hello/);

// Numbers
assertGreater(5, 3);
assertLess(3, 5);

Error Assertions

// Synchronous throws
assertThrows(
  () => JSON.parse("invalid json"),
  SyntaxError,
  "Unexpected token"
);

// Async rejects
await assertRejects(
  async () => {
    const res = await fetch("http://localhost:99999");
    if (!res.ok) throw new Error("Fetch failed");
  },
  Error,
  "Fetch failed"
);

// Just check that it throws, without type or message
assertThrows(() => { throw new Error("anything"); });

Test Structure

Basic Tests

// format_test.ts
import { assertEquals } from "jsr:@std/assert";
import { formatCurrency, formatDate, slugify } from "./format.ts";

Deno.test("formatCurrency formats USD with 2 decimal places", () => {
  assertEquals(formatCurrency(10, "USD"), "$10.00");
});

Deno.test("formatCurrency handles zero", () => {
  assertEquals(formatCurrency(0, "USD"), "$0.00");
});

Deno.test("formatDate formats as YYYY-MM-DD", () => {
  assertEquals(formatDate(new Date("2026-01-15")), "2026-01-15");
});

Deno.test("slugify converts spaces to hyphens", () => {
  assertEquals(slugify("Hello World"), "hello-world");
});

Deno.test("slugify removes special characters", () => {
  assertEquals(slugify("Hello, World!"), "hello-world");
});

Subtests

Group related assertions with t.step():

// user_test.ts
import { assertEquals, assertExists, assertThrows } from "jsr:@std/assert";
import { createUser, validateUser, User } from "./user.ts";

Deno.test("User creation and validation", async (t) => {
  await t.step("creates user with required fields", () => {
    const user = createUser({ name: "Ada Lovelace", email: "ada@example.com" });
    assertExists(user.id);
    assertEquals(user.name, "Ada Lovelace");
    assertEquals(user.email, "ada@example.com");
  });

  await t.step("generates unique IDs for each user", () => {
    const user1 = createUser({ name: "User 1", email: "user1@example.com" });
    const user2 = createUser({ name: "User 2", email: "user2@example.com" });
    assertNotEquals(user1.id, user2.id);
  });

  await t.step("throws when email is missing", () => {
    assertThrows(
      () => createUser({ name: "Ada" } as any),
      Error,
      "email is required"
    );
  });

  await t.step("throws when name is missing", () => {
    assertThrows(
      () => createUser({ email: "ada@example.com" } as any),
      Error,
      "name is required"
    );
  });
});

Subtests are reported individually — you see exactly which step failed without running the others.

Async Tests

Deno handles async tests natively:

Deno.test("fetches user data from API", async () => {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const user = await response.json();

  assertEquals(response.status, 200);
  assertExists(user.name);
  assertExists(user.email);
});

For tests that need setup and teardown:

Deno.test("reads and writes to temp file", async () => {
  const tempFile = await Deno.makeTempFile({ suffix: ".txt" });

  try {
    await Deno.writeTextFile(tempFile, "hello world");
    const content = await Deno.readTextFile(tempFile);
    assertEquals(content, "hello world");
  } finally {
    await Deno.remove(tempFile);
  }
});

The try/finally pattern ensures cleanup even on test failure.

Test Permissions

Deno requires explicit permissions. Tests that access the network, file system, or environment variables need permission flags:

# Allow network access for tests that fetch URLs
deno <span class="hljs-built_in">test --allow-net

<span class="hljs-comment"># Allow file system access
deno <span class="hljs-built_in">test --allow-read --allow-write

<span class="hljs-comment"># Allow environment variable access
deno <span class="hljs-built_in">test --allow-env

<span class="hljs-comment"># Allow all (use sparingly)
deno <span class="hljs-built_in">test --allow-all

You can also specify permissions per-test:

Deno.test({
  name: "reads config file",
  permissions: { read: ["/etc/app/config.json"] },
  fn() {
    // test code
  },
});

Restricting permissions per-test documents exactly what each test requires and prevents tests from accidentally accessing things they shouldn't.

Test Options

Deno.test accepts an options object for finer control:

// Skip a test (and explain why)
Deno.test({
  name: "integration test — requires running database",
  ignore: !Deno.env.get("DATABASE_URL"),
  async fn() {
    // ...
  },
});

// Mark a test as expected to fail
Deno.test({
  name: "known broken behavior — tracked in issue #42",
  fn() {
    assertEquals(buggyFunction(), "expected");
  },
});

// Sanitizers — catch resource leaks and async operations that outlive the test
Deno.test({
  name: "opens connection",
  sanitizeOps: true,      // catches pending async operations
  sanitizeResources: true, // catches unclosed file handles, connections
  async fn() {
    const conn = await Deno.connect({ port: 8080 });
    // conn must be closed before test ends
    conn.close();
  },
});

The sanitizeOps and sanitizeResources options (enabled by default) catch a common class of bugs: tests that leave async operations or resources open. If your test opens a connection and doesn't close it, the test fails with a clear error.

Test Filtering

Run specific tests without modifying code:

# Filter by test name (partial match)
deno <span class="hljs-built_in">test --filter <span class="hljs-string">"format"

<span class="hljs-comment"># Filter by file
deno <span class="hljs-built_in">test format_test.ts user_test.ts

<span class="hljs-comment"># Run tests in a specific directory
deno <span class="hljs-built_in">test tests/unit/

Code Coverage

# Collect coverage data
deno <span class="hljs-built_in">test --coverage=cov_dir

<span class="hljs-comment"># Generate HTML report
deno coverage cov_dir --html

<span class="hljs-comment"># Print coverage to terminal
deno coverage cov_dir

Open cov_dir/index.html for a line-by-line coverage breakdown. The report shows which branches were exercised and which were missed.

Testing File Structure

Deno doesn't enforce a specific test structure, but two patterns work well:

Co-located tests (tests next to source files):

src/
  format.ts
  format_test.ts
  user.ts
  user_test.ts
  api/
    posts.ts
    posts_test.ts

Separate test directory:

src/
  format.ts
  user.ts
tests/
  unit/
    format_test.ts
    user_test.ts
  integration/
    api_test.ts

Co-location makes it obvious which test covers which module. Separate directories make it easier to run unit and integration tests independently.

What Tests Don't Cover

Deno tests verify your code logic. They don't verify what happens after you deploy your application:

  • A production environment may have different permissions configured
  • Network requests that pass in tests may fail in production due to TLS or DNS differences
  • File path assumptions that work locally may break on the deployment target
  • Deno KV in production (FoundationDB) behaves differently from the in-memory test instance

Production Monitoring with HelpMeTest

HelpMeTest monitors your Deno app continuously:

curl -fsSL https://helpmetest.com/install | bash

Write tests in plain English that run against your deployed app:

Go to https://mydenoapp.com/api/health
Verify the response status is 200
Verify the response contains "ok"

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