Automatically Restoring Vitest Mocks with using
Forgetting to restore a spy in Vitest lets mock state leak into other tests. Since Vitest 3.2.0, assigning the return value of `vi.spyOn()` to a `using` declaration restores the original implementation automatically when the scope exits.
When a Vitest test deals with code that depends on an external API or the current time, you often reach for vi.spyOn() to mock a method on an object temporarily. Because a spy overwrites the target method, you have to put the original implementation back once the test finishes. Forget to restore it, and another test ends up calling the mocked implementation, which makes your results depend on the order the tests happen to run in.
describe("Test suite A", () => {
test("Test A1", () => {
const logSpy = vi.spyOn(logger, "log").mockReturnValue("mocked message");
// ...the body of the test
// You must remember to call mockRestore()!
logSpy.mockRestore();
});
});Calling mockRestore() in every single test block is a bit of a chore, though. There is also a subtler problem: if a failed assertion or an unexpected exception ends the test before mockRestore() is reached, the restoration never happens.
Since Vitest 3.2.0, you can assign the return value of vi.spyOn() to JavaScript's using declaration. A spy declared with using has mockRestore() called automatically when the scope exits.
describe("Test suite A", () => {
test("Test A1", () => {
using logSpy = vi.spyOn(logger, "log").mockReturnValue("mocked message");
// ...the body of the test
// mockRestore() is called automatically when the scope exits
});
});This article looks at how to use using to restore Vitest spies automatically.
Putting the original implementation back
Let's start by reviewing how to restore a spy created with vi.spyOn() by hand. As the target of our spy, we'll use a logger.log() method that formats a message.
export const logger = {
log(message: string) {
return `[info] ${message}`;
},
};The test below spies on logger.log() and changes its return value to "mocked message". Calling mockRestore() at the end of the test puts the original logger.log() back.
import { expect, test, vi } from "vitest";
import { logger } from "./logger";
test("logs a message", () => {
const logSpy = vi
.spyOn(logger, "log")
.mockReturnValue("mocked message");
expect(logger.log("hello")).toBe("mocked message");
expect(logSpy).toHaveBeenCalledOnce();
logSpy.mockRestore();
});This code restores correctly as long as the test runs to completion. But if a failed assertion or an unexpected exception ends the test before mockRestore() is reached, the restoration never runs.
One reliable way to restore spies is to call vi.restoreAllMocks() from afterEach().
import { afterEach, vi } from "vitest";
afterEach(() => {
vi.restoreAllMocks();
});The drawback is that the afterEach() block is usually defined far away from the test block itself, which makes the relationship — "where does the spy I created in this test get restored?" — a little harder to follow.
Alternatively, setting restoreMocks: true in your Vitest config restores spies before each test. Both of these are a good fit when you want blanket restoration across the whole test suite. That said, you may not always be in a position to change a global setting so casually. When you want to control restoration on a per-test basis, a using declaration is a handy option.
Restoring spies automatically with a using declaration
A using declaration lets you have mockRestore() called automatically at the end of a block. Let's declare the return value of vi.spyOn() with using instead of const.
import { expect, test, vi } from "vitest";
import { logger } from "./logger";
test("logs a message", () => {
using logSpy = vi
.spyOn(logger, "log")
.mockReturnValue("mocked message");
expect(logger.log("hello")).toBe("mocked message");
expect(logSpy).toHaveBeenCalledOnce();
});When the scope of the test function exits, mockRestore() is called automatically as logSpy's disposal step. Unlike restoring in afterEach(), the declaration sits right next to the test block that needs it, which makes the relationship much easier to see.
using is a new piece of JavaScript syntax that comes from a proposal called Explicit Resource Management. The specification work is finished, and it is on track to land in ES2027. It was proposed as a way to tie resources that need explicit cleanup — file handles, streams, and the like — to the scope of a variable.
An object assigned to using must have a method keyed by Symbol.dispose.
const resource = {
[Symbol.dispose]() {
console.log("Disposed the resource");
},
};Once the block containing using value = resource finishes, resource[Symbol.dispose]() is called. Just like try / finally, the disposal step still runs even if an exception is thrown inside the block.
{
using value = resource;
//...the body of the block
// resource[Symbol.dispose]() is called on the way out of the block
}In Vitest 3.2.0, the mock functions returned by vi.spyOn() and vi.fn() implement Symbol.dispose, and [Symbol.dispose]() calls mockRestore(). That is what makes a spy declared with using restore itself automatically when the scope exits.
using is still relatively new syntax. Type checking requires TypeScript 5.2, and running it requires Node.js 24.0 or later.
Summary
- Since Vitest 3.2.0, declaring the return value of
vi.spyOn()withusingcausesmockRestore()to be called automatically when the scope ends - The disposal step of
usingruns not only on normal completion but also when the scope is left via an exception or an early return


