Skip to main content
Deno has a built-in test runner that makes it easy to write and run tests without additional dependencies. Tests use the native Deno.test() API and can include assertions from the standard library.

Basic Testing

Writing Your First Test

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

Deno.test("addition works", () => {
  const result = 2 + 2;
  assertEquals(result, 4);
});

Deno.test("subtraction works", () => {
  assertEquals(10 - 5, 5);
});
Run tests:
deno test

Test File Naming

Deno automatically discovers test files matching these patterns:
  • *_test.ts
  • *_test.tsx
  • *_test.js
  • *_test.jsx
  • *.test.ts
  • *.test.tsx
  • *.test.js
  • *.test.jsx

Test Structure

Basic Test Format

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

Deno.test("test name", () => {
  // Test code here
  assertEquals(actual, expected);
});

Test with Options

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

Deno.test({
  name: "user validation test",
  fn: () => {
    const user = { name: "Alice", age: 30 };
    assertEquals(user.name, "Alice");
  },
});

Async Tests

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

Deno.test("async operation", async () => {
  const data = await fetchData();
  assertEquals(data.status, "success");
});

Deno.test("with async function", async () => {
  const response = await fetch("https://api.example.com");
  assertEquals(response.status, 200);
});

Assertions

Deno’s standard library provides comprehensive assertion functions:
import {
  assertEquals,
  assertNotEquals,
  assertStrictEquals,
  assertExists,
  assertInstanceOf,
  assertStringIncludes,
  assertMatch,
  assertArrayIncludes,
  assertObjectMatch,
  assertThrows,
  assertRejects,
  assert,
  fail,
} from "jsr:@std/assert";

// Basic equality
Deno.test("equality", () => {
  assertEquals(2 + 2, 4);
  assertNotEquals(2 + 2, 5);
  assertStrictEquals("hello", "hello");
});

// Existence
Deno.test("exists", () => {
  const user = { name: "Alice" };
  assertExists(user);
  assertExists(user.name);
});

// Type checking
Deno.test("types", () => {
  const date = new Date();
  assertInstanceOf(date, Date);
});

// Strings
Deno.test("strings", () => {
  assertStringIncludes("Hello World", "World");
  assertMatch("hello123", /[a-z]+\d+/);
});

// Arrays and objects
Deno.test("collections", () => {
  assertArrayIncludes([1, 2, 3, 4], [2, 3]);
  assertObjectMatch(
    { name: "Alice", age: 30, email: "[email protected]" },
    { name: "Alice", age: 30 }
  );
});

// Errors
Deno.test("throws", () => {
  assertThrows(
    () => {
      throw new Error("Oops!");
    },
    Error,
    "Oops!"
  );
});

Deno.test("async rejects", async () => {
  await assertRejects(
    async () => {
      throw new Error("Async error");
    },
    Error,
    "Async error"
  );
});

// Custom assertions
Deno.test("custom", () => {
  assert(2 + 2 === 4, "Math should work");
});

Test Permissions

Specify Permissions Per Test

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

Deno.test({
  name: "file read test",
  permissions: { read: true },
  fn: async () => {
    const data = await Deno.readTextFile("data.txt");
    assertEquals(typeof data, "string");
  },
});

Deno.test({
  name: "network test",
  permissions: { net: ["api.example.com"] },
  async fn() {
    const response = await fetch("https://api.example.com");
    assertEquals(response.ok, true);
  },
});

Deno.test({
  name: "multiple permissions",
  permissions: {
    read: ["/tmp"],
    write: ["/tmp"],
    net: true,
  },
  fn: () => {
    // Test code
  },
});

No Permissions Test

Deno.test({
  name: "pure logic test",
  permissions: "none",
  fn: () => {
    assertEquals(2 + 2, 4);
  },
});

Test Organization

Test Steps

Organize complex tests with steps:
import { assertEquals } from "jsr:@std/assert";

Deno.test("user workflow", async (t) => {
  await t.step("create user", async () => {
    const user = await createUser({ name: "Alice" });
    assertEquals(user.name, "Alice");
  });

  await t.step("update user", async () => {
    const user = await updateUser(1, { age: 30 });
    assertEquals(user.age, 30);
  });

  await t.step("delete user", async () => {
    await deleteUser(1);
    const user = await getUser(1);
    assertEquals(user, null);
  });
});

Before and After Hooks

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

let db: Database;

Deno.test("database tests", async (t) => {
  // Setup
  db = await Database.connect();
  await db.migrate();

  await t.step("insert data", async () => {
    const id = await db.insert({ name: "Alice" });
    assertEquals(typeof id, "number");
  });

  await t.step("query data", async () => {
    const users = await db.query("SELECT * FROM users");
    assertEquals(users.length > 0, true);
  });

  // Cleanup
  await db.close();
});

Filtering Tests

Run Specific Tests

# Run tests matching a pattern
deno test --filter "user"

# Run specific test file
deno test user_test.ts

# Run multiple files
deno test user_test.ts post_test.ts

# Run all tests in a directory
deno test tests/

Ignore Tests

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

Deno.test({
  name: "work in progress",
  ignore: true,
  fn: () => {
    // This test will be skipped
  },
});

// Conditionally ignore
Deno.test({
  name: "windows only test",
  ignore: Deno.build.os !== "windows",
  fn: () => {
    // Only runs on Windows
  },
});

Focus on Specific Tests

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

Deno.test({
  name: "focused test",
  only: true,
  fn: () => {
    // Only this test will run when --allow-only flag is used
  },
});
Run with:
deno test --allow-only

Test Configuration

Configure in deno.json

deno.json
{
  "test": {
    "include": ["src/**/*_test.ts"],
    "exclude": ["src/testdata/", "src/fixtures/"],
    "permissions": {
      "read": true,
      "net": ["localhost", "api.example.com"]
    }
  }
}

Named Permission Sets

deno.json
{
  "permissions": {
    "test-all": {
      "read": true,
      "write": true,
      "net": true
    },
    "test-restricted": {
      "read": ["./test-data"],
      "net": ["localhost:8000"]
    }
  },
  "test": {
    "permissions": "test-all"
  }
}

Coverage

Generate Coverage

# Run tests with coverage
deno test --coverage=coverage/

# View coverage report
deno coverage coverage/

# Generate HTML report
deno coverage coverage/ --html

# Generate LCOV report
deno coverage coverage/ --lcov --output=coverage.lcov

Coverage in CI

# GitHub Actions example
deno test --coverage=coverage/
deno coverage coverage/ --lcov --output=coverage.lcov

# Upload to Codecov
bash <(curl -s https://codecov.io/bash)

Sanitizers

Deno includes built-in sanitizers to catch common issues:

Resource Sanitizer

Detects resource leaks (files, network connections):
Deno.test({
  name: "resource leak test",
  sanitizeResources: false, // Disable if intentional
  async fn() {
    const file = await Deno.open("data.txt");
    // Forgot to close - sanitizer will catch this
  },
});

Operation Sanitizer

Detects pending async operations:
Deno.test({
  name: "async leak test",
  sanitizeOps: false, // Disable if intentional
  fn() {
    setTimeout(() => console.log("leak"), 1000);
    // Sanitizer will catch pending operation
  },
});

Exit Sanitizer

Detects Deno.exit() calls:
Deno.test({
  name: "exit test",
  sanitizeExit: false,
  fn() {
    Deno.exit(0);
  },
});

Reporters

Built-in Reporters

# Pretty reporter (default)
deno test

# Dot reporter
deno test --reporter=dot

# TAP reporter
deno test --reporter=tap

# JUnit reporter
deno test --reporter=junit --junit-path=./report.xml

Multiple Reporters

deno test --reporter=pretty --reporter=junit --junit-path=report.xml

Watch Mode

Automatically re-run tests on file changes:
# Watch all tests
deno test --watch

# Watch specific files
deno test --watch user_test.ts

# Watch with filter
deno test --watch --filter="user"

Parallel Testing

Tests run in parallel by default:
# Control parallelism
deno test --parallel

# Run sequentially
deno test --jobs=1

# Custom job count
deno test --jobs=4

Real-World Example

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

Deno.test("User operations", async (t) => {
  await t.step("create user", async () => {
    const user = await createUser({
      name: "Alice",
      email: "[email protected]",
      age: 30,
    });

    assertExists(user.id);
    assertEquals(user.name, "Alice");
    assertEquals(user.email, "[email protected]");
  });

  await t.step("get user", async () => {
    const user = await getUser(1);
    assertExists(user);
    assertEquals(user.name, "Alice");
  });

  await t.step("update user", async () => {
    const user = await updateUser(1, { age: 31 });
    assertEquals(user.age, 31);
  });

  await t.step("invalid email fails", async () => {
    await assertRejects(
      async () => {
        await createUser({
          name: "Bob",
          email: "invalid-email",
          age: 25,
        });
      },
      Error,
      "Invalid email"
    );
  });
});

Deno.test({
  name: "user permissions",
  permissions: { read: true, net: ["localhost:5432"] },
  async fn() {
    const user = await getUser(1);
    assertExists(user);
  },
});

Best Practices

Test naming

Use descriptive test names that explain what is being tested

One assertion per test

Keep tests focused with specific assertions

Use test steps

Organize complex tests with steps for better readability

Clean up resources

Always close files, connections, and other resources

Minimal permissions

Grant only the permissions each test needs

Enable coverage

Track code coverage to ensure thorough testing

Configuration Example

deno.json
{
  "tasks": {
    "test": "deno test",
    "test:watch": "deno test --watch",
    "test:coverage": "deno test --coverage=coverage && deno coverage coverage/"
  },
  "test": {
    "include": ["src/**/*_test.ts", "tests/**/*.test.ts"],
    "exclude": ["src/testdata/**", "tests/fixtures/**"],
    "permissions": {
      "read": true,
      "net": ["localhost", "127.0.0.1"]
    }
  },
  "imports": {
    "@std/assert": "jsr:@std/assert@^1.0.0"
  }
}

Build docs developers (and LLMs) love