Testing with Vitest
A TevmProvider boots in milliseconds and needs no binary, no port, and no beforeAll process
management. That makes it a good default backend for tests of any code that takes an ethers
provider or signer.
Setup
pnpm add -D vitest
pnpm add @tevm/ethers ethers tevm viem@2.46.0// vitest.config.ts
import { } from 'vitest/config'
export default ({
: {
: 'node',
: 20_000,
},
})Forked tests hit the network on first read, so a timeout above the default is worth setting.
A fresh EVM per test
Create the provider in beforeEach so no test can observe another test's state.
import { } from '@tevm/ethers'
import { } from '@tevm/utils'
import { , , } from 'ethers'
import { , , , } from 'vitest'
('payments', () => {
let :
(async () => {
= await .createMemoryProvider({
: { : 'auto' },
})
})
('transfers ether', async () => {
const = new ([0], )
const = `0x${'42'.(20)}`
const = await .({ : , : ('1') })
const = await .()
(?.).(1)
((await .getBalance())).('1.0')
})
('starts from a clean slate', async () => {
(await .getBalance(`0x${'42'.(20)}`)).(0n)
})
})Testing a contract
import { , } from '@tevm/ethers'
import { } from '@tevm/contract'
import { , } from '@tevm/utils'
import { } from 'ethers'
import { , , , } from 'vitest'
('ERC20', () => {
let :
let :
(async () => {
= await .createMemoryProvider({
: { : 'auto' },
})
const = await .tevm.deploy({
....('Token', 'TKN'),
: .,
: .,
: [0].,
: true,
})
await .tevm.deal({
: .createdAddress,
: [0].,
: 1000n,
})
= new (.createdAddress, ., new ([0], ))
})
('exposes metadata', async () => {
(await .name()).('Token')
(await .symbol()).('TKN')
(await .decimals()).(18n)
})
('emits Transfer on transfer', async () => {
const = await .transfer(`0x${'42'.(20)}`, 100n)
await .wait()
(await .balanceOf(`0x${'42'.(20)}`)).(100n)
const = await .queryFilter('Transfer', 0, 'latest')
().(1)
([0].args.value).(100n)
})
})Forked tests without repeated RPC cost
Fork once, dump the state, and reload it in each test. After the first run nothing touches the network.
import { } from '@tevm/ethers'
import { } from 'viem'
import { , , , , } from 'vitest'
('forked mainnet', () => {
let : <<['tevm']['dumpState']>>
let :
(async () => {
const = await .createMemoryProvider({
: {
: ('https://mainnet.optimism.io')({}),
: 154_847_000n,
},
})
// Touch the state the tests need so it is captured in the dump.
await .getBalance('0x0000000000000000000000000000000000000001')
= await .tevm.dumpState()
})
(async () => {
= await .createMemoryProvider({ : { : 'auto' } })
await .tevm.loadState({ : .state })
})
('sees forked balances', async () => {
(typeof (await .getBalance('0x0000000000000000000000000000000000000001'))).('bigint')
})
})Tips
- Pin the fork block. An unpinned fork makes tests non-deterministic.
- Prefer
miningConfig: { type: 'auto' }in tests unless you are specifically testing mempool behavior; otherwisetx.wait()never resolves until you callprovider.tevm.mine(). - Use
dealinstead of impersonating a whale when you only need a balance — it is faster and has no upstream dependency. - Don't share a provider across test files. Each file gets its own process-level module state in Vitest, but sharing within a file leaks state between tests.

