Deno Unit Testing: Mocking, Spies and Stubs with std/mock (2026)

Deno Unit Testing: Mocking, Spies and Stubs with std/mock (2026)

Deno's standard library provides @std/mock for test doubles — spies, stubs, and fake timers. These let you isolate units under test from their dependencies without external mocking libraries.

This guide covers everything in @std/mock: spying on function calls, stubbing module methods, mocking fetch, and controlling time in tests.

Installing @std/mock

import {
  spy,
  stub,
  assertSpyCalls,
  assertSpyCall,
  returnsNext,
  resolvesNext,
  FakeTime,
} from "jsr:@std/mock";

No installation needed — Deno fetches the module on first run and caches it.

Spies

A spy wraps a function, records all calls to it, and passes through to the original implementation. Use spies when you want to verify that a function was called without changing its behavior.

// notification.ts
export interface NotificationService {
  send(to: string, message: string): Promise<void>;
}

export class UserService {
  constructor(private notifications: NotificationService) {}

  async register(email: string, name: string): Promise<void> {
    // ... create user in DB ...
    await this.notifications.send(email, `Welcome, ${name}!`);
  }
}
// notification_test.ts
import { spy, assertSpyCalls, assertSpyCall } from "jsr:@std/mock";
import { assertEquals } from "jsr:@std/assert";

Deno.test("UserService sends welcome notification on register", async () => {
  const notificationService = {
    send: spy(async (_to: string, _message: string) => {}),
  };

  const userService = new UserService(notificationService);
  await userService.register("ada@example.com", "Ada Lovelace");

  // Verify send was called exactly once
  assertSpyCalls(notificationService.send, 1);

  // Verify it was called with the right arguments
  assertSpyCall(notificationService.send, 0, {
    args: ["ada@example.com", "Welcome, Ada Lovelace!"],
  });
});

Deno.test("UserService does not send duplicate notifications", async () => {
  const notificationService = {
    send: spy(async (_to: string, _message: string) => {}),
  };

  const userService = new UserService(notificationService);
  await userService.register("ada@example.com", "Ada");

  // Exactly 1 call, no duplicates
  assertSpyCalls(notificationService.send, 1);
});

Stubs

A stub replaces a function's implementation for the duration of a test. Use stubs when you need a dependency to return a specific value or when you want to prevent a side effect (like a network request or database write).

// weather.ts
export async function getWeatherDescription(city: string): Promise<string> {
  const res = await fetch(`https://api.weather.com/current?city=${city}`);
  const data = await res.json();
  return `${data.description} at ${data.tempF}°F`;
}
// weather_test.ts
import { stub } from "jsr:@std/mock";
import { assertEquals } from "jsr:@std/assert";
import * as weatherModule from "./weather.ts";

Deno.test("getWeatherDescription formats response correctly", async () => {
  const fetchStub = stub(
    globalThis,
    "fetch",
    async () =>
      new Response(
        JSON.stringify({ description: "Sunny", tempF: 72 }),
        { status: 200, headers: { "Content-Type": "application/json" } }
      )
  );

  try {
    const result = await weatherModule.getWeatherDescription("London");
    assertEquals(result, "Sunny at 72°F");
  } finally {
    fetchStub.restore();
  }
});

Always call stub.restore() in a finally block — this ensures the original function is restored even if the test fails.

Stubbing Object Methods

Stub methods on objects to control what they return:

// db_test.ts
import { stub, assertSpyCalls } from "jsr:@std/mock";
import { assertEquals } from "jsr:@std/assert";
import { db } from "./db.ts";
import { UserRepository } from "./user_repository.ts";

Deno.test("UserRepository.findById returns user data", async () => {
  const mockUser = { id: "user-1", name: "Ada Lovelace", email: "ada@test.com" };

  const queryStub = stub(db, "query", async () => [mockUser]);

  try {
    const repo = new UserRepository(db);
    const user = await repo.findById("user-1");

    assertEquals(user?.name, "Ada Lovelace");
    assertSpyCalls(queryStub, 1);
  } finally {
    queryStub.restore();
  }
});

Deno.test("UserRepository.findById returns null when user not found", async () => {
  const queryStub = stub(db, "query", async () => []);

  try {
    const repo = new UserRepository(db);
    const user = await repo.findById("nonexistent");

    assertEquals(user, null);
  } finally {
    queryStub.restore();
  }
});

returnsNext and resolvesNext

For stubs that need to return different values on successive calls, use returnsNext (synchronous) or resolvesNext (async):

import { stub, returnsNext, resolvesNext } from "jsr:@std/mock";
import { assertEquals } from "jsr:@std/assert";

Deno.test("retry logic retries on failure and succeeds eventually", async () => {
  let callCount = 0;
  const fetchStub = stub(globalThis, "fetch", resolvesNext([
    // First call: network error
    Promise.reject(new Error("Network timeout")),
    // Second call: server error
    new Response(null, { status: 503 }),
    // Third call: success
    new Response(JSON.stringify({ status: "ok" }), { status: 200 }),
  ]));

  try {
    const result = await fetchWithRetry("/api/status", { retries: 3 });
    assertEquals(result.status, "ok");
  } finally {
    fetchStub.restore();
  }
});

Mocking fetch with returnsNext

A common pattern is to mock fetch for HTTP client tests:

// api_client.ts
export class ApiClient {
  constructor(private baseUrl: string) {}

  async get<T>(path: string): Promise<T> {
    const res = await fetch(`${this.baseUrl}${path}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  }

  async post<T>(path: string, body: unknown): Promise<T> {
    const res = await fetch(`${this.baseUrl}${path}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  }
}
// api_client_test.ts
import { stub, assertSpyCall } from "jsr:@std/mock";
import { assertEquals, assertRejects } from "jsr:@std/assert";
import { ApiClient } from "./api_client.ts";

function mockResponse(data: unknown, status = 200) {
  return new Response(JSON.stringify(data), {
    status,
    headers: { "Content-Type": "application/json" },
  });
}

Deno.test("ApiClient.get returns parsed JSON on success", async () => {
  const fetchStub = stub(
    globalThis,
    "fetch",
    async () => mockResponse({ id: 1, name: "Ada" })
  );

  try {
    const client = new ApiClient("https://api.example.com");
    const result = await client.get<{ id: number; name: string }>("/users/1");

    assertEquals(result.name, "Ada");
  } finally {
    fetchStub.restore();
  }
});

Deno.test("ApiClient.get throws on HTTP error", async () => {
  const fetchStub = stub(
    globalThis,
    "fetch",
    async () => new Response(null, { status: 404 })
  );

  try {
    const client = new ApiClient("https://api.example.com");
    await assertRejects(
      () => client.get("/users/nonexistent"),
      Error,
      "HTTP 404"
    );
  } finally {
    fetchStub.restore();
  }
});

Deno.test("ApiClient.post sends JSON body", async () => {
  const fetchStub = stub(
    globalThis,
    "fetch",
    async (url, init) => mockResponse({ id: 2 }, 201)
  );

  try {
    const client = new ApiClient("https://api.example.com");
    await client.post("/users", { name: "Grace Hopper" });

    assertSpyCall(fetchStub, 0, {
      args: ["https://api.example.com/users", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name: "Grace Hopper" }),
      }],
    });
  } finally {
    fetchStub.restore();
  }
});

Fake Timers

Test time-dependent code without waiting for real time to pass:

// rate_limiter.ts
export class RateLimiter {
  private requests: number[] = [];

  constructor(
    private readonly maxRequests: number,
    private readonly windowMs: number
  ) {}

  isAllowed(): boolean {
    const now = Date.now();
    const windowStart = now - this.windowMs;

    this.requests = this.requests.filter((t) => t > windowStart);

    if (this.requests.length >= this.maxRequests) return false;

    this.requests.push(now);
    return true;
  }
}
// rate_limiter_test.ts
import { FakeTime } from "jsr:@std/mock";
import { assertEquals } from "jsr:@std/assert";
import { RateLimiter } from "./rate_limiter.ts";

Deno.test("RateLimiter allows requests up to the limit", () => {
  using time = new FakeTime();
  const limiter = new RateLimiter(3, 60_000); // 3 requests per minute

  assertEquals(limiter.isAllowed(), true);
  assertEquals(limiter.isAllowed(), true);
  assertEquals(limiter.isAllowed(), true);
  // 4th request should be rejected
  assertEquals(limiter.isAllowed(), false);
});

Deno.test("RateLimiter resets after time window expires", () => {
  using time = new FakeTime();
  const limiter = new RateLimiter(3, 60_000);

  // Exhaust the limit
  limiter.isAllowed();
  limiter.isAllowed();
  limiter.isAllowed();
  assertEquals(limiter.isAllowed(), false);

  // Advance 61 seconds
  time.tick(61_000);

  // Window has reset — requests allowed again
  assertEquals(limiter.isAllowed(), true);
});

Deno.test("RateLimiter uses sliding window", () => {
  using time = new FakeTime();
  const limiter = new RateLimiter(3, 60_000);

  limiter.isAllowed(); // t=0
  time.tick(30_000);
  limiter.isAllowed(); // t=30s
  limiter.isAllowed(); // t=30s
  assertEquals(limiter.isAllowed(), false); // limit reached

  // After 31s, first request (t=0) falls outside the window
  time.tick(31_000);

  // Should allow 1 more
  assertEquals(limiter.isAllowed(), true);
});

The using keyword (disposable resources, Deno 2+) automatically restores the real time after the test. For older Deno versions, call time.restore() in a finally block.

Patterns for Testable Code

The key to easy mocking in Deno is dependency injection. Instead of calling fetch directly, accept it as a parameter:

// Hard to test — fetch is hardcoded
async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

// Easy to test — fetch is injected
async function fetchUser(
  id: string,
  fetcher: typeof fetch = fetch
) {
  const res = await fetcher(`/api/users/${id}`);
  return res.json();
}

In tests, pass a mock fetcher. In production, the default fetch is used.

The same applies to time (Date.now), random values, and any other global state your code reads.

Production Monitoring with HelpMeTest

Unit tests with mocks verify logic in isolation. After deploying your Deno app, the real dependencies (databases, external APIs, file systems) may behave differently than your mocks assumed.

HelpMeTest monitors your deployed Deno app:

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

Free tier: 10 tests, unlimited health checks.
Pro: $100/month
— unlimited tests, 24/7 monitoring.


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

Read more