On this page
Testing in isolation with mocks
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";
// Define types for better code quality
interface User {
name: string;
}
interface Database {
save: (user: User) => Promise<User & { id: number }>;
}
// Function to test
function saveUser(
user: User,
database: Database,
): Promise<User & { id: number }> {
return database.save(user);
}
// Test with a mock
Deno.test("saveUser calls database.save", async () => {
// Create a mock database with a spy on the save method
const mockDatabase: Database = {
save: spy((user: User) => Promise.resolve({ id: 1, ...user })),
};
const user: User = { name: "Test User" };
const result = await saveUser(user, mockDatabase);
// Verify the mock was called correctly
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:
- The save method was called exactly once
- The first argument of the call was the
user
object we passed in - The result contains both the original user data and the added ID
We were able to test the saveUser
operation without setting up or tearing down
any complex database state.
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";
// Define types for better code quality
interface User {
name: string;
role: string;
}
// Original function
function getCurrentUser(userId: string): User {
// Implementation that might involve database calls
return { name: "Real User", role: "admin" };
}
// Function we want to test
function hasAdminAccess(userId: string): boolean {
const user = getCurrentUser(userId);
return user.role === "admin";
}
Deno.test("hasAdminAccess with stubbed user", () => {
// Create a stub that replaces getCurrentUser
const getUserStub: Stub<typeof getCurrentUser> = stub(
globalThis,
"getCurrentUser",
// Return a test user with non-admin role
() => ({ name: "Test User", role: "guest" }),
);
try {
// Test with the stubbed function
const result = hasAdminAccess("user123");
assertEquals(result, false);
// You can also change the stub's behavior during the test
getUserStub.restore(); // Remove first stub
const adminStub = stub(
globalThis,
"getCurrentUser",
() => ({ name: "Admin User", role: "admin" }),
);
try {
const adminResult = hasAdminAccess("admin456");
assertEquals(adminResult, true);
} finally {
adminStub.restore();
}
} finally {
// Always restore the original function
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:
- Calls the
getCurrentUser()
function to get a user object - Checks if the user's role is "admin"
- Returns a boolean indicating whether the user has admin access
Next 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.
Stubbing environment variables Jump to heading
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 that depends on environment variables and time
function generateReport() {
const environment = Deno.env.get("ENVIRONMENT") || "development";
const timestamp = new Date().toISOString();
return {
environment,
generatedAt: timestamp,
data: {/* report data */},
};
}
Deno.test("report generation with controlled environment", () => {
// Stub 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);
});
// Stub time
const dateStub = stub(
Date.prototype,
"toISOString",
() => "2023-06-15T12:00:00Z",
);
try {
const report = generateReport();
// Verify results with controlled values
assertEquals(report.environment, "production");
assertEquals(report.generatedAt, "2023-06-15T12:00:00Z");
} finally {
// Always restore stubs to prevent affecting other tests
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, fakeTime } from "jsr:@std/testing/mock";
// Function that depends on the current time
function isWeekend(): boolean {
const date = new Date();
const day = date.getDay();
return day === 0 || day === 6; // 0 is Sunday, 6 is Saturday
}
// Function that works with timeouts
function delayedGreeting(callback: (message: string) => void): void {
setTimeout(() => {
callback("Hello after delay");
}, 1000); // 1 second delay
}
Deno.test("time-dependent tests", () => {
// Create a fake time starting at a specific date (a Monday)
const mockedTime: FakeTime = fakeTime(new Date("2023-05-01T12:00:00Z"));
try {
// Test with the mocked Monday
assertEquals(isWeekend(), false);
// Move time forward to Saturday
mockedTime.tick(5 * 24 * 60 * 60 * 1000); // Advance 5 days
assertEquals(isWeekend(), true);
// Test async operations with timers
let greeting = "";
delayedGreeting((message) => {
greeting = message;
});
// Advance time to trigger the timeout immediately
mockedTime.tick(1000);
assertEquals(greeting, "Hello after delay");
} finally {
// Always restore the real time
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:
- Calendar or date-based features, such as scheduling, appointments or expiration dates
- Code with timeouts or intervals, such as polling, delayed operations or debouncing
- Animations or transitions such as testing the completion of timed visual effects
Like with stubs, always restore the real time functions after your tests using
the restore()
method to avoid affecting other tests.
Advanced mocking patterns Jump to heading
Partial mocking Jump to heading
Sometimes you only want to mock certain methods of an object while keeping others intact:
import { assertEquals } from "jsr:@std/assert";
import { spy } from "jsr:@std/testing/mock";
class UserService {
async getUser(id: string) {
// Complex database query
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 spies", async () => {
const service = new UserService();
// Only mock the getUser method
const getUserSpy = spy(
service,
"getUser",
() => Promise.resolve({ id: "test-id", name: "Mocked User" }),
);
try {
// The formatUser method will still use the real implementation
const result = await service.getUserFormatted("test-id");
assertEquals(result, {
id: "test-id",
name: "Mocked User",
displayName: "MOCKED USER",
});
// Verify getUser was called with the right arguments
assertEquals(getUserSpy.calls.length, 1);
assertEquals(getUserSpy.calls[0].args[0], "test-id");
} finally {
getUserSpy.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";
// Function that uses fetch
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;
// Create a response that the mock fetch will return
const mockResponse = new Response(
JSON.stringify({ id: "123", name: "John Doe" }),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
// Replace fetch with a stubbed version
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 {
// Restore original fetch
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:
- Validates user credentials
- Calls an API to authenticate
- Stores tokens with expiration times
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 { FakeTime, fakeTime, spy, stub } from "jsr:@std/testing/mock";
// The service we want to test
class AuthService {
private token: string | null = null;
private expiresAt: Date | null = null;
async login(username: string, password: string): Promise<string> {
// Validate inputs
if (!username || !password) {
throw new Error("Username and password are required");
}
// Call authentication API
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();
// Store token with expiration (1 hour)
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();
// Mock successful response
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) => {
// Verify correct data is being sent
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", () => {
const authService = new AuthService();
const time = fakeTime(new Date("2023-01-01T12:00:00Z"));
try {
// Mock the login process to set token directly
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";
},
);
// Login and verify token
authService.login("user", "pass").then(() => {
const token = authService.getToken();
assertEquals(token, "fake-token");
// Advance time past expiration
time.tick(61 * 60 * 1000);
// Token should now be expired
assertRejects(
() => {
authService.getToken();
},
Error,
"Token expired",
);
});
} finally {
time.restore();
(authService.login as any).restore();
}
});
});
This code defines AuthService
class with three main functionalities:
- Login - Validates credentials, calls an API, and stores a token with an expiration time
- GetToken - Returns the token if valid and not expired
- Logout - Clears the token and expiration
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: