On this page
Stubbing in tests
Stubbing is a powerful technique for isolating the code you're testing by replacing functions with controlled implementations. While spies monitor function calls without changing behavior, stubs go a step further by completely replacing the original implementation, allowing you to simulate specific conditions or behaviors during testing.
What are stubs? Jump to heading
Stubs are fake implementations that replace real functions during testing. They let you:
- Control what values functions return
- Simulate errors or specific edge cases
- Prevent external services like databases or APIs from being called
- Test code paths that would be difficult to trigger with real implementations
Deno provides robust stubbing capabilities through the Standard Library's testing tools.
Basic stub usage Jump to heading
Here's a simple example demonstrating how to stub a function:
import { assertEquals } from "jsr:@std/assert";
import { stub } from "jsr:@std/testing/mock";
// Original function
function getUserName(id: number): string {
// In a real app, this might call a database
return "Original User";
}
// Function under test
function greetUser(id: number): string {
const name = getUserName(id);
return `Hello, ${name}!`;
}
Deno.test("greetUser with stubbed getUserName", () => {
// Create a stub that returns a controlled value
const getUserNameStub = stub(globalThis, "getUserName", () => "Test User");
try {
// Test with the stubbed function
const greeting = greetUser(123);
assertEquals(greeting, "Hello, Test User!");
} finally {
// Always restore the original function
getUserNameStub.restore();
}
});
In this example, we:
- Import the necessary functions from Deno's standard library
- Create a stub for the
getUserName
function that returns "Test User" instead of calling the real implementation - Call our function under test, which will use the stubbed implementation
- Verify the result meets our expectations
- Restore the original function to prevent affecting other tests
Using stubs in a testing scenario Jump to heading
Let's look at a more practical example with a UserRepository
class that
interacts with a database:
import { assertSpyCalls, returnsNext, stub } from "jsr:@std/testing/mock";
import { assertThrows } from "jsr:@std/assert";
type User = {
id: number;
name: string;
};
// This represents our database access layer
const database = {
getUserById(id: number): User | undefined {
// In a real app, this would query a database
return { id, name: "Ada Lovelace" };
},
};
// The class we want to test
class UserRepository {
static findOrThrow(id: number): User {
const user = database.getUserById(id);
if (!user) {
throw new Error("User not found");
}
return user;
}
}
Deno.test("findOrThrow method throws when the user was not found", () => {
// Stub the database.getUserById function to return undefined
using dbStub = stub(database, "getUserById", returnsNext([undefined]));
// We expect this function call to throw an error
assertThrows(() => UserRepository.findOrThrow(1), Error, "User not found");
// Verify the stubbed function was called once
assertSpyCalls(dbStub, 1);
});
In this example:
- We're testing the
findOrThrow
method, which should throw an error when a user is not found - We stub
database.getUserById
to returnundefined
, simulating a missing user - We verify that
findOrThrow
throws the expected error - We also check that the database method was called exactly once
Note that we're using the using
keyword with stub
, which is a convenient way
to ensure the stub is automatically restored when it goes out of scope.
Advanced stub techniques Jump to heading
Returning different values on subsequent calls Jump to heading
Sometimes you want a stub to return different values each time it's called:
import { returnsNext, stub } from "jsr:@std/testing/mock";
import { assertEquals } from "jsr:@std/assert";
Deno.test("stub with multiple return values", () => {
const fetchDataStub = stub(
globalThis,
"fetchData",
// Return these values in sequence
returnsNext(["first result", "second result", "third result"]),
);
try {
assertEquals(fetchData(), "first result");
assertEquals(fetchData(), "second result");
assertEquals(fetchData(), "third result");
} finally {
fetchDataStub.restore();
}
});
Stubbing with implementation logic Jump to heading
You can also provide custom logic in your stub implementations:
import { stub } from "jsr:@std/testing/mock";
import { assertEquals } from "jsr:@std/assert";
Deno.test("stub with custom implementation", () => {
// Create a counter to track how many times the stub is called
let callCount = 0;
const calculateStub = stub(
globalThis,
"calculate",
(a: number, b: number) => {
callCount++;
return a + b * 2; // Custom implementation
},
);
try {
const result = calculate(5, 10);
assertEquals(result, 25); // 5 + (10 * 2)
assertEquals(callCount, 1);
} finally {
calculateStub.restore();
}
});
Stubbing API calls and external services Jump to heading
One of the most common uses of stubs is to replace API calls during testing:
import { assertEquals } from "jsr:@std/assert";
import { stub } from "jsr:@std/testing/mock";
async function fetchUserData(id: string) {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return await response.json();
}
Deno.test("fetchUserData with stubbed fetch", async () => {
const mockResponse = new Response(
JSON.stringify({ id: "123", name: "Jane Doe" }),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
// Replace global fetch with a stubbed version
const fetchStub = stub(
globalThis,
"fetch",
() => Promise.resolve(mockResponse),
);
try {
const user = await fetchUserData("123");
assertEquals(user, { id: "123", name: "Jane Doe" });
} finally {
fetchStub.restore();
}
});
Best practices Jump to heading
-
Always restore stubs: Use
try/finally
blocks or theusing
keyword to ensure stubs are restored, even if tests fail. -
Use stubs for external dependencies: Stub out database calls, API requests, or file system operations to make tests faster and more reliable.
-
Keep stubs simple: Stubs should return predictable values that let you test specific scenarios.
-
Combine with spies when needed: Sometimes you need to both replace functionality (stub) and track calls (spy).
-
Stub at the right level: Stub at the interface boundary rather than deep within implementation details.
🦕 Stubs are a powerful tool for isolating your code during testing, allowing you to create deterministic test environments and easily test edge cases. By replacing real implementations with controlled behavior, you can write more focused, reliable tests that run quickly and consistently.
For more testing resources, check out: