On this page
Testing in isolation with mocks
This guide builds on thebasics of testing in Deno to focus specificallyon mocking techniques that help you isolate your code during testing.
For effective unit testing, you'll often need to "mock" the data that your codeinteracts with. Mocking is a technique used in testing where you replace realdata with simulated versions that you can control. This is particularly usefulwhen testing components that interact with external services, such as APIs ordatabases.
Deno provideshelpful mocking utilitiesthrough the Deno Standard Library, making your tests easier to write, morereliable and faster.
SpyingJump to heading
In Deno, you canspy on afunction to track how it's called during test execution. Spies don't change howa function behaves, but they record important details like how many times thefunction was called and what arguments were passed to it.
By using spies, you can verify that your code interacts correctly with itsdependencies without setting up complex infrastructure.
In the following example we will test a function calledsaveUser(), whichtakes a user object and a database object and calls the database'ssavemethod:
import{ assertEquals}from"jsr:@std/assert";import{ assertSpyCalls, spy}from"jsr:@std/testing/mock";// Define types for better code qualityinterfaceUser{ name:string;}interfaceDatabase{save:(user: User)=>Promise<User&{ id:number}>;}// Function to testfunctionsaveUser( user: User, database: Database,):Promise<User&{ id:number}>{return database.save(user);}// Test with a mockDeno.test("saveUser calls database.save",async()=>{// Create a mock database with a spy on the save methodconst mockDatabase={ save:spy((user: User)=>Promise.resolve({ id:1,...user})),};const user: User={ name:"Test User"};const result=awaitsaveUser(user, mockDatabase);// Verify the mock was called correctlyassertSpyCalls(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 assertequality and to create and verify spy functions.
The mock database is a stand-in for a real database object, with asave methodthat is wrapped in aspy. The spy function tracks calls to the method, recordsarguments passed to it and executes the underlying implementation (in this casereturning a promise with theuser and anid).
The test callssaveUser() with the mock data and we use assertions to verifythat:
- The save method was called exactly once
- The first argument of the call was the
userobject we passed in - The result contains both the original user data and the added ID
We were able to test thesaveUser operation without setting up or tearing downany complex database state.
Clearing spiesJump to heading
When working with multiple tests that use spies, it's important to reset orclear spies between tests to avoid interference. The Deno testing libraryprovides a simple way to restore all spies to their original state using therestore() 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",()=>{// Create a spy on a functionconst myFunction=spy((x:number)=> x*2);// Use the spyconst result=myFunction(5);assertEquals(result,10);assertSpyCalls(myFunction,1);// After testing, restore the spytry{// Test code using the spy// ...}finally{// Always clean up spies myFunction.restore();}});Method spies are disposable, they can automatically restore themselves with theusing keyword. This approach means that you do not need to wrap yourassertions in a try statement to ensure you restore the methods before the testsfinish.
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,};// The spy will automatically be restored when it goes out of scope using addSpy=spy(calculator,"add");// Use the spyconst sum= calculator.add(3,4);assertEquals(sum,7);assertSpyCalls(addSpy,1);assertEquals(addSpy.calls[0].args,[3,4]);// No need for try/finally blocks - the spy will be restored automatically});Deno.test("using multiple disposable spies",()=>{const calculator={add:(a:number, b:number)=> a+ b,multiply:(a:number, b:number)=> a* b,};// Both spies will automatically be restored using addSpy=spy(calculator,"add"); using multiplySpy=spy(calculator,"multiply"); calculator.add(5,3); calculator.multiply(4,2);assertSpyCalls(addSpy,1);assertSpyCalls(multiplySpy,1);// No cleanup code needed});For cases where you have multiple spies that don't support theusing keyword,you can track them in an array and restore them all at once:
Deno.test("multiple spies cleanup",()=>{const spies=[];// Create spiesconst functionA=spy((x:number)=> x+1); spies.push(functionA);const objectB={method:(x:number)=> x*2,};const spyB=spy(objectB,"method"); spies.push(spyB);// Use the spies in tests// ...// Clean up all spies at the endtry{// Test code using spies}finally{// Restore all spies spies.forEach((spyFn)=> spyFn.restore());}});By properly cleaning up spies, you ensure that each test starts with a cleanstate and avoid side effects between tests.
StubbingJump to heading
While spies track method calls without changing behavior, stubs replace theoriginal implementation entirely.Stubbing is a form of mockingwhere you temporarily replace a function or method with a controlledimplementation. This allows you to simulate specific conditions or behaviors andreturn predetermined values. It can also be used when you need to overrideenvironment-dependent functionality.
In Deno, you can create stubs using thestub function from the standardtesting library:
import{ assertEquals}from"jsr:@std/assert";import{ Stub, stub}from"jsr:@std/testing/mock";// Define types for better code qualityinterfaceUser{ name:string; role:string;}// Original functionfunctiongetCurrentUser(userId:string): User{// Implementation that might involve database callsreturn{ name:"Real User", role:"admin"};}// Function we want to testfunctionhasAdminAccess(userId:string):boolean{const user=getCurrentUser(userId);return user.role==="admin";}Deno.test("hasAdminAccess with stubbed user",()=>{// Create a stub that replaces getCurrentUserconst 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 functionconst result=hasAdminAccess("user123");assertEquals(result,false);// You can also change the stub's behavior during the test getUserStub.restore();// Remove first stubconst 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 weset up the function we're going to stub. In a real application this mightconnect to a database, make an API call, or perform other operations that we maywant to avoid during testing.
We set up the function under test, in this case thehasAdminAccess() 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 namedhasAdminAccess with a stubbed user and set up astub for thegetCurrentUser function. This will replace the realimplementation with one that returns a user with aguest role.
We run the test with the stubbed function, it will callhasAdminAccess with auser ID. Even though the real function would return a user withadmin role,our stub returnsguest, so we can assert thathasAdminAccess returnsfalse(since our stub returns a non-admin user).
We can change the stub behavior to returnadmin instead and assert that thefunction now returnstrue.
At the end we use afinally block to ensure the original function is restoredso that we don't accidentally affect other tests.
Stubbing environment variablesJump 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 timefunctiongenerateReport(){const environment= Deno.env.get("ENVIRONMENT")||"development";const timestamp=newDate().toISOString();return{ environment, generatedAt: timestamp, data:{/* report data */},};}Deno.test("report generation with controlled environment",()=>{// Stub environmentconst originalEnv= Deno.env.get;const envStub=stub(Deno.env,"get",(key:string)=>{if(key==="ENVIRONMENT")return"production";returnoriginalEnv.call(Deno.env, key);});// Stub timeconst dateStub=stub( Date.prototype,"toISOString",()=>"2023-06-15T12:00:00Z",);try{const report=generateReport();// Verify results with controlled valuesassertEquals(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 timeJump to heading
Time-dependent code can be challenging to test because it may produce differentresults based on when the test runs. Deno provides aFakeTime utility that allows you tosimulate 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, anddelayedGreeting() which calls a callback after a 1-second delay:
import{ assertEquals}from"jsr:@std/assert";import{ FakeTime}from"jsr:@std/testing/time";// Function that depends on the current timefunctionisWeekend():boolean{const date=newDate();const day= date.getDay();return day===0|| day===6;// 0 is Sunday, 6 is Saturday}// Function that works with timeoutsfunctiondelayedGreeting(callback:(message:string)=>void):void{setTimeout(()=>{callback("Hello after delay");},1000);// 1 second delay}Deno.test("time-dependent tests",()=>{ using fakeTime=newFakeTime();// Create a fake time starting at a specific date (a Monday)const mockedTime: FakeTime=fakeTime(newDate("2023-05-01T12:00:00Z"));try{// Test with the mocked MondayassertEquals(isWeekend(),false);// Move time forward to Saturday mockedTime.tick(5*24*60*60*1000);// Advance 5 daysassertEquals(isWeekend(),true);// Test async operations with timerslet 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 withfakeTime which sets the starting date to May 1, 2023, (which was a Monday). Itreturns aFakeTime controller object that lets us manipulate time.
We run tests with the mocked Monday and will see that theisWeekend functionreturnsfalse. Then we can advance time to Saturday and run the test again toverify thatisWeekend returnstrue.
ThefakeTime function replaces JavaScript's timing functions (Date,setTimeout,setInterval, etc.) with versions you can control. This allowsyou 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 thesystem clock and can speed up tests by advancing time instantly instead ofwaiting for real timeouts.
Fake time is particularly useful for testing:
- Calendar or date-based features, such as scheduling, appointments orexpiration dates
- Code with timeouts or intervals, such as polling, delayed operations ordebouncing
- Animations or transitions such as testing the completion of timed visualeffects
Like with stubs, always restore the real time functions after your tests usingtherestore() method to avoid affecting other tests.
Advanced mocking patternsJump to heading
Partial mockingJump to heading
Sometimes you only want to mock certain methods of an object while keepingothers intact:
import{ assertEquals}from"jsr:@std/assert";import{ stub}from"jsr:@std/testing/mock";classUserService{asyncgetUser(id:string){// Complex database queryreturn{ id, name:"Database User"};}asyncformatUser(user:{ id:string; name:string}){return{...user, displayName: user.name.toUpperCase(),};}asyncgetUserFormatted(id:string){const user=awaitthis.getUser(id);returnthis.formatUser(user);}}Deno.test("partial mocking with stubs",async()=>{const service=newUserService();// Only mock the getUser methodconst getUserMock=stub( service,"getUser",()=>Promise.resolve({ id:"test-id", name:"Mocked User"}),);try{// The formatUser method will still use the real implementationconst result=await service.getUserFormatted("test-id");assertEquals(result,{ id:"test-id", name:"Mocked User", displayName:"MOCKED USER",});// Verify getUser was called with the right argumentsassertEquals(getUserMock.calls.length,1);assertEquals(getUserMock.calls[0].args[0],"test-id");}finally{ getUserMock.restore();}});Mocking fetch requestsJump to heading
Testing code that makes HTTP requests often requires mocking thefetch API:
import{ assertEquals}from"jsr:@std/assert";import{ stub}from"jsr:@std/testing/mock";// Function that uses fetchasyncfunctionfetchUserData(userId:string){const response=awaitfetch(`https://api.example.com/users/${userId}`);if(!response.ok){thrownewError(`Failed to fetch user:${response.status}`);}returnawait response.json();}Deno.test("mocking fetch API",async()=>{const originalFetch= globalThis.fetch;// Create a response that the mock fetch will returnconst mockResponse=newResponse(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=awaitfetchUserData("123");assertEquals(result,{ id:"123", name:"John Doe"});}finally{// Restore original fetch globalThis.fetch= originalFetch;}});Real-world exampleJump to heading
Let's put everything together in a more comprehensive example. We'll test a userauthentication service that:
- Validates user credentials
- Calls an API to authenticate
- Stores tokens with expiration times
In the example below, we'll create a fullAuthService class that handles userlogin, token management, and authentication. We'll test it thoroughly usingvarious mocking techniques covered earlier: stubbing fetch requests, spying onmethods, and manipulating time to test token expiration - all within organizedtest steps.
Deno's testing API provides a usefult.step() function that allows you toorganize your tests into logical steps or sub-tests. This makes complex testsmore readable and helps pinpoint exactly which part of a test is failing. Eachstep can have its own assertions and will be reported separately in the testoutput.
import{ assertEquals, assertRejects}from"jsr:@std/assert";import{ spy, stub}from"jsr:@std/testing/mock";import{ FakeTime}from"jsr:@std/testing/time";// The service we want to testclassAuthService{private token:string|null=null;private expiresAt: Date|null=null;asynclogin(username:string, password:string):Promise<string>{// Validate inputsif(!username||!password){thrownewError("Username and password are required");}// Call authentication APIconst response=awaitfetch("https://api.example.com/login",{ method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({ username, password}),});if(!response.ok){thrownewError(`Authentication failed:${response.status}`);}const data=await response.json();// Store token with expiration (1 hour)this.token= data.token;this.expiresAt=newDate(Date.now()+60*60*1000);returnthis.token;}getToken():string{if(!this.token||!this.expiresAt){thrownewError("Not authenticated");}if(newDate()>this.expiresAt){this.token=null;this.expiresAt=null;thrownewError("Token expired");}returnthis.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=newAuthService();awaitassertRejects(()=> authService.login("","password"), Error,"Username and password are required",);});await t.step("login should handle API calls",async()=>{const authService=newAuthService();// Mock successful responseconst mockResponse=newResponse(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 sentconst body= options?.bodyasstring;const parsedBody=JSON.parse(body);assertEquals(parsedBody.username,"testuser");assertEquals(parsedBody.password,"password123");returnPromise.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=newFakeTime();const authService=newAuthService();const time=fakeTime(newDate("2023-01-01T12:00:00Z"));try{// Mock the login process to set token directly authService.login=spy( authService,"login",async()=>{(authServiceasany).token="fake-token";(authServiceasany).expiresAt=newDate( 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 expiredassertRejects(()=>{ authService.getToken();}, Error,"Token expired",);});}finally{ time.restore();(authService.loginasany).restore();}});});This code definesAuthService class with three main functionalities:
- Login - Validates credentials, calls an API, and stores a token with anexpiration 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 logicalsteps, each testing a different aspect of the service; credentialvalidation, 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 duringtesting. By mastering these mocking techniques, you'll be able to write morereliable tests that run faster and don't depend on external services.
For more testing resources, check out: