This guide builds on the basics of testing in Deno to focus specifically on mocking techniques that help you isolate your code during testing.
For effective unit testing, you'll often need to "mock" the data that your code interacts with. Mocking is a technique used in testing where you replace real data with simulated versions that you can control. This is particularly useful when testing components that interact with external services, such as APIs or databases.
Deno provides helpful mocking utilities through the Deno Standard Library, making your tests easier to write, more reliable and faster.
Spying Jump to heading#In Deno, you can spy
on a function to track how it's called during test execution. Spies don't change how a function behaves, but they record important details like how many times the function was called and what arguments were passed to it.
By using spies, you can verify that your code interacts correctly with its dependencies without setting up complex infrastructure.
In the following example we will test a function called saveUser()
, which takes a user object and a database object and calls the database's save
method:
import { assertEquals } from "jsr:@std/assert";
import { assertSpyCalls, spy } from "jsr:@std/testing/mock";
interface User {
name: string;
}
interface Database {
save: (user: User) => Promise<User & { id: number }>;
}
function saveUser(
user: User,
database: Database,
): Promise<User & { id: number }> {
return database.save(user);
}
Deno.test("saveUser calls database.save", async () => {
const mockDatabase = {
save: spy((user: User) => Promise.resolve({ id: 1, ...user })),
};
const user: User = { name: "Test User" };
const result = await saveUser(user, mockDatabase);
assertSpyCalls(mockDatabase.save, 1);
assertEquals(mockDatabase.save.calls[0].args[0], user);
assertEquals(result, { id: 1, name: "Test User" });
});
We import the necessary functions from the Deno Standard Library to assert equality and to create and verify spy functions.
The mock database is a stand-in for a real database object, with a save
method that is wrapped in a spy
. The spy function tracks calls to the method, records arguments passed to it and executes the underlying implementation (in this case returning a promise with the user
and an id
).
The test calls saveUser()
with the mock data and we use assertions to verify that:
user
object we passed inWe were able to test the saveUser
operation without setting up or tearing down any complex database state.
When working with multiple tests that use spies, it's important to reset or clear spies between tests to avoid interference. The Deno testing library provides a simple way to restore all spies to their original state using the restore()
method.
Here's how to clear a spy after you're done with it:
import { assertEquals } from "jsr:@std/assert";
import { assertSpyCalls, spy } from "jsr:@std/testing/mock";
Deno.test("spy cleanup example", () => {
const myFunction = spy((x: number) => x * 2);
const result = myFunction(5);
assertEquals(result, 10);
assertSpyCalls(myFunction, 1);
try {
} finally {
myFunction.restore();
}
});
Method spies are disposable, they can automatically restore themselves with the using
keyword. This approach means that you do not need to wrap your assertions in a try statement to ensure you restore the methods before the tests finish.
import { assertEquals } from "jsr:@std/assert";
import { assertSpyCalls, spy } from "jsr:@std/testing/mock";
Deno.test("using disposable spies", () => {
const calculator = {
add: (a: number, b: number) => a + b,
multiply: (a: number, b: number) => a * b,
};
using addSpy = spy(calculator, "add");
const sum = calculator.add(3, 4);
assertEquals(sum, 7);
assertSpyCalls(addSpy, 1);
assertEquals(addSpy.calls[0].args, [3, 4]);
});
Deno.test("using multiple disposable spies", () => {
const calculator = {
add: (a: number, b: number) => a + b,
multiply: (a: number, b: number) => a * b,
};
using addSpy = spy(calculator, "add");
using multiplySpy = spy(calculator, "multiply");
calculator.add(5, 3);
calculator.multiply(4, 2);
assertSpyCalls(addSpy, 1);
assertSpyCalls(multiplySpy, 1);
});
For cases where you have multiple spies that don't support the using
keyword, you can track them in an array and restore them all at once:
Deno.test("multiple spies cleanup", () => {
const spies = [];
const functionA = spy((x: number) => x + 1);
spies.push(functionA);
const objectB = {
method: (x: number) => x * 2,
};
const spyB = spy(objectB, "method");
spies.push(spyB);
try {
} finally {
spies.forEach((spyFn) => spyFn.restore());
}
});
By properly cleaning up spies, you ensure that each test starts with a clean state and avoid side effects between tests.
Stubbing Jump to heading#While spies track method calls without changing behavior, stubs replace the original implementation entirely. Stubbing is a form of mocking where you temporarily replace a function or method with a controlled implementation. This allows you to simulate specific conditions or behaviors and return predetermined values. It can also be used when you need to override environment-dependent functionality.
In Deno, you can create stubs using the stub
function from the standard testing library:
import { assertEquals } from "jsr:@std/assert";
import { Stub, stub } from "jsr:@std/testing/mock";
interface User {
name: string;
role: string;
}
function getCurrentUser(userId: string): User {
return { name: "Real User", role: "admin" };
}
function hasAdminAccess(userId: string): boolean {
const user = getCurrentUser(userId);
return user.role === "admin";
}
Deno.test("hasAdminAccess with stubbed user", () => {
const getUserStub: Stub<typeof getCurrentUser> = stub(
globalThis,
"getCurrentUser",
() => ({ name: "Test User", role: "guest" }),
);
try {
const result = hasAdminAccess("user123");
assertEquals(result, false);
getUserStub.restore();
const adminStub = stub(
globalThis,
"getCurrentUser",
() => ({ name: "Admin User", role: "admin" }),
);
try {
const adminResult = hasAdminAccess("admin456");
assertEquals(adminResult, true);
} finally {
adminStub.restore();
}
} finally {
getUserStub.restore();
}
});
Here we import the necessary functions from the Deno Standard Library, then we set up the function we're going to stub. In a real application this might connect to a database, make an API call, or perform other operations that we may want to avoid during testing.
We set up the function under test, in this case the hasAdminAccess()
function. We want to test whether it:
getCurrentUser()
function to get a user objectNext we create a test named hasAdminAccess with a stubbed user
and set up a stub for the getCurrentUser
function. This will replace the real implementation with one that returns a user with a guest
role.
We run the test with the stubbed function, it will call hasAdminAccess
with a user ID. Even though the real function would return a user with admin
role, our stub returns guest
, so we can assert that hasAdminAccess
returns false
(since our stub returns a non-admin user).
We can change the stub behavior to return admin
instead and assert that the function now returns true
.
At the end we use a finally
block to ensure the original function is restored so that we don't accidentally affect other tests.
For deterministic testing, you often need to control environment variables. Deno's Standard Library provides utilities to achieve this:
import { assertEquals } from "jsr:@std/assert";
import { stub } from "jsr:@std/testing/mock";
function generateReport() {
const environment = Deno.env.get("ENVIRONMENT") || "development";
const timestamp = new Date().toISOString();
return {
environment,
generatedAt: timestamp,
data: {},
};
}
Deno.test("report generation with controlled environment", () => {
const originalEnv = Deno.env.get;
const envStub = stub(Deno.env, "get", (key: string) => {
if (key === "ENVIRONMENT") return "production";
return originalEnv.call(Deno.env, key);
});
const dateStub = stub(
Date.prototype,
"toISOString",
() => "2023-06-15T12:00:00Z",
);
try {
const report = generateReport();
assertEquals(report.environment, "production");
assertEquals(report.generatedAt, "2023-06-15T12:00:00Z");
} finally {
envStub.restore();
dateStub.restore();
}
});
Faking time Jump to heading#
Time-dependent code can be challenging to test because it may produce different results based on when the test runs. Deno provides a FakeTime
utility that allows you to simulate the passage of time and control date-related functions during tests.
The example below demonstrates how to test time-dependent functions: isWeekend()
, which returns true if the current day is Saturday or Sunday, and delayedGreeting()
which calls a callback after a 1-second delay:
import { assertEquals } from "jsr:@std/assert";
import { FakeTime } from "jsr:@std/testing/time";
function isWeekend(): boolean {
const date = new Date();
const day = date.getDay();
return day === 0 || day === 6;
}
function delayedGreeting(callback: (message: string) => void): void {
setTimeout(() => {
callback("Hello after delay");
}, 1000);
}
Deno.test("time-dependent tests", () => {
using fakeTime = new FakeTime();
const mockedTime: FakeTime = fakeTime(new Date("2023-05-01T12:00:00Z"));
try {
assertEquals(isWeekend(), false);
mockedTime.tick(5 * 24 * 60 * 60 * 1000);
assertEquals(isWeekend(), true);
let greeting = "";
delayedGreeting((message) => {
greeting = message;
});
mockedTime.tick(1000);
assertEquals(greeting, "Hello after delay");
} finally {
mockedTime.restore();
}
});
Here we set up a test which creates a controlled time environment with fakeTime
which sets the starting date to May 1, 2023, (which was a Monday). It returns a FakeTime
controller object that lets us manipulate time.
We run tests with the mocked Monday and will see that the isWeekend
function returns false
. Then we can advance time to Saturday and run the test again to verify that isWeekend
returns true
.
The fakeTime
function replaces JavaScript's timing functions (Date
, setTimeout
, setInterval
, etc.) with versions you can control. This allows you to test code with specific dates or times regardless of when the test runs. This powerful technique means you will avoid flaky tests that depend on the system clock and can speed up tests by advancing time instantly instead of waiting for real timeouts.
Fake time is particularly useful for testing:
Like with stubs, always restore the real time functions after your tests using the restore()
method to avoid affecting other tests.
Sometimes you only want to mock certain methods of an object while keeping others intact:
import { assertEquals } from "jsr:@std/assert";
import { stub } from "jsr:@std/testing/mock";
class UserService {
async getUser(id: string) {
return { id, name: "Database User" };
}
async formatUser(user: { id: string; name: string }) {
return {
...user,
displayName: user.name.toUpperCase(),
};
}
async getUserFormatted(id: string) {
const user = await this.getUser(id);
return this.formatUser(user);
}
}
Deno.test("partial mocking with stubs", async () => {
const service = new UserService();
const getUserMock = stub(
service,
"getUser",
() => Promise.resolve({ id: "test-id", name: "Mocked User" }),
);
try {
const result = await service.getUserFormatted("test-id");
assertEquals(result, {
id: "test-id",
name: "Mocked User",
displayName: "MOCKED USER",
});
assertEquals(getUserMock.calls.length, 1);
assertEquals(getUserMock.calls[0].args[0], "test-id");
} finally {
getUserMock.restore();
}
});
Mocking fetch requests Jump to heading#
Testing code that makes HTTP requests often requires mocking the fetch
API:
import { assertEquals } from "jsr:@std/assert";
import { stub } from "jsr:@std/testing/mock";
async function fetchUserData(userId: string) {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return await response.json();
}
Deno.test("mocking fetch API", async () => {
const originalFetch = globalThis.fetch;
const mockResponse = new Response(
JSON.stringify({ id: "123", name: "John Doe" }),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
globalThis.fetch = stub(
globalThis,
"fetch",
(_input: string | URL | Request, _init?: RequestInit) =>
Promise.resolve(mockResponse),
);
try {
const result = await fetchUserData("123");
assertEquals(result, { id: "123", name: "John Doe" });
} finally {
globalThis.fetch = originalFetch;
}
});
Real-world example Jump to heading#
Let's put everything together in a more comprehensive example. We'll test a user authentication service that:
In the example below, we'll create a full AuthService
class that handles user login, token management, and authentication. We'll test it thoroughly using various mocking techniques covered earlier: stubbing fetch requests, spying on methods, and manipulating time to test token expiration - all within organized test steps.
Deno's testing API provides a useful t.step()
function that allows you to organize your tests into logical steps or sub-tests. This makes complex tests more readable and helps pinpoint exactly which part of a test is failing. Each step can have its own assertions and will be reported separately in the test output.
import { assertEquals, assertRejects } from "jsr:@std/assert";
import { spy, stub } from "jsr:@std/testing/mock";
import { FakeTime } from "jsr:@std/testing/time";
class AuthService {
private token: string | null = null;
private expiresAt: Date | null = null;
async login(username: string, password: string): Promise<string> {
if (!username || !password) {
throw new Error("Username and password are required");
}
const response = await fetch("https://api.example.com/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error(`Authentication failed: ${response.status}`);
}
const data = await response.json();
this.token = data.token;
this.expiresAt = new Date(Date.now() + 60 * 60 * 1000);
return this.token;
}
getToken(): string {
if (!this.token || !this.expiresAt) {
throw new Error("Not authenticated");
}
if (new Date() > this.expiresAt) {
this.token = null;
this.expiresAt = null;
throw new Error("Token expired");
}
return this.token;
}
logout(): void {
this.token = null;
this.expiresAt = null;
}
}
Deno.test("AuthService comprehensive test", async (t) => {
await t.step("login should validate credentials", async () => {
const authService = new AuthService();
await assertRejects(
() => authService.login("", "password"),
Error,
"Username and password are required",
);
});
await t.step("login should handle API calls", async () => {
const authService = new AuthService();
const mockResponse = new Response(
JSON.stringify({ token: "fake-jwt-token" }),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
const fetchStub = stub(
globalThis,
"fetch",
(_url: string | URL | Request, options?: RequestInit) => {
const body = options?.body as string;
const parsedBody = JSON.parse(body);
assertEquals(parsedBody.username, "testuser");
assertEquals(parsedBody.password, "password123");
return Promise.resolve(mockResponse);
},
);
try {
const token = await authService.login("testuser", "password123");
assertEquals(token, "fake-jwt-token");
} finally {
fetchStub.restore();
}
});
await t.step("token expiration should work correctly", () => {
using fakeTime = new FakeTime();
const authService = new AuthService();
const time = fakeTime(new Date("2023-01-01T12:00:00Z"));
try {
authService.login = spy(
authService,
"login",
async () => {
(authService as any).token = "fake-token";
(authService as any).expiresAt = new Date(
Date.now() + 60 * 60 * 1000,
);
return "fake-token";
},
);
authService.login("user", "pass").then(() => {
const token = authService.getToken();
assertEquals(token, "fake-token");
time.tick(61 * 60 * 1000);
assertRejects(
() => {
authService.getToken();
},
Error,
"Token expired",
);
});
} finally {
time.restore();
(authService.login as any).restore();
}
});
});
This code defines AuthService
class with three main functionalities:
The testing structure is organized as a single main test with three logical steps, each testing a different aspect of the service; credential validation, API call handling and token expiration.
🦕 Effective mocking is essential for writing reliable, maintainable unit tests. Deno provides several powerful tools to help you isolate your code during testing. By mastering these mocking techniques, you'll be able to write more reliable tests that run faster and don't depend on external services.
For more testing resources, check out:
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4