Auto-Cleanup with using
Spies and mocks need to be restored after the test that installed them, otherwise state leaks between tests. The usual approaches are an afterEach(() => vi.restoreAllMocks()) at the suite level or a per-test onTestFinished(() => spy.mockRestore()) inline.
If your runtime supports Explicit Resource Management (Node.js 24+, or via TypeScript 5.2+ in modern bundlers), there's a tighter option: declare the spy with using instead of const, and restoration happens automatically when the block exits.
This works for vi.spyOn, vi.fn, and vi.doMock. 3.2.0+
Pattern
import { expect, it, vi } from 'vitest'
function debug(message: string) {
console.log(message)
}
it('calls console.log', () => {
using spy = vi.spyOn(console, 'log').mockImplementation(() => {})
debug('message')
expect(spy).toHaveBeenCalled()
})
// console.log is restored here without an afterEachThe same pattern works with vi.doMock, which returns a disposable that queues an unmock when the scope exits:
import { expect, it, vi } from 'vitest'
it('uses the mocked module, then the real one', async () => {
{
using _mock = vi.doMock('./users', () => ({
loadUser: () => ({ id: '1', name: 'Alice' }),
}))
const { loadUser } = await import('./users')
expect(loadUser('alice').name).toBe('Alice')
}
// ./users is unmocked from here on
})Scoped to any block
using is block-scoped, so you can install a spy for just part of a test. This is the case neither afterEach nor onTestFinished covers, since both run after the test ends:
import { expect, it, vi } from 'vitest'
it('only mocks fetch for the auth call', async () => {
// real fetch here
await preloadConfig()
{
using fetchSpy = vi.spyOn(globalThis, 'fetch')
.mockResolvedValue(new Response('{"ok":true}'))
await login('alice', 'secret')
expect(fetchSpy).toHaveBeenCalledOnce()
}
// real fetch is back
await reportSuccess()
})This is also a way to avoid turning on the global restoreMocks: true config when only a handful of calls actually need restoration.
Compatibility
using requires support for the TC39 Explicit Resource Management proposal:
- TypeScript ≥ 5.2 (with
target: 'es2022'or higher and thedisposablelib included by default). - Node.js ≥ 24 (or Node 22+ with
--harmony-style flags) for native runtime support.
If your environment doesn't support it yet, the closest equivalent for whole-test cleanup is onTestFinished, which registers the cleanup inline and runs after the test completes regardless of pass or failure:
import { expect, it, onTestFinished, vi } from 'vitest'
it('calls console.log', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
onTestFinished(() => spy.mockRestore())
debug('message')
expect(spy).toHaveBeenCalled()
})onTestFinished can't tear down a spy mid-test the way using can, so the block-scoped pattern above remains specific to ERM.
