| title | description | url |
|---|---|---|
Stubbing in tests |
Learn how to use stubs in Deno to isolate code during testing by replacing function implementations with controlled behavior |
/examples/stubbing_tutorial/ |
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.
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.
Here's a simple example demonstrating how to stub a function:
import { assertEquals } from "jsr:@std/assert";
import { stub } from "jsr:@std/testing/mock";
// Wrap dependencies so they can be stubbed safely from tests.
const deps = {
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 = deps.getUserName(_id);
return `Hello, ${name}!`;
}
Deno.test("greetUser with stubbed getUserName", () => {
// Create a stub that returns a controlled value
const getUserNameStub = stub(deps, "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
getUserNamefunction 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
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
findOrThrowmethod, which should throw an error when a user is not found - We stub
database.getUserByIdto returnundefined, simulating a missing user - We verify that
findOrThrowthrows 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.
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();
}
});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();
}
});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();
}
});-
Always restore stubs: Use
try/finallyblocks or theusingkeyword 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: