Local Testing
Tevm runs a deterministic Ethereum chain in the same process as your test runner. The examples below use the viem-compatible MemoryClient, which is the recommended testing surface in tevm@1.0.0-rc.151.
Setup
npm install tevm@1.0.0-rc.151 viem
npm install --save-dev vitestNo RPC server or separate chain process is required.
Transfer, Mine, and Read a Receipt
Tevm actions can simulate a call without changing canonical state. Set addToMempool: true to create a pending transaction, then mine it explicitly.
import { createMemoryClient, parseEther, PREFUNDED_ACCOUNTS } from 'tevm'
import { expect, test } from 'vitest'
test('mines an ETH transfer', async () => {
const client = createMemoryClient({
miningConfig: { type: 'manual' },
})
const alice = PREFUNDED_ACCOUNTS[0].address
const bob = '0x1111111111111111111111111111111111111111'
const { txHash } = await client.tevmCall({
from: alice,
to: bob,
value: parseEther('1'),
addToMempool: true,
})
expect(txHash).toBeDefined()
if (!txHash) throw new Error('transaction was not added to the txpool')
await client.tevmMine({ blockCount: 1 })
const receipt = await client.getTransactionReceipt({ hash: txHash })
expect(receipt.status).toBe('success')
expect(await client.getBalance({ address: bob })).toBe(parseEther('1'))
})createMemoryClient() is synchronous. Await client.tevmReady() only when a test wants to finish initialization eagerly, especially before reading from a fork.
Test a Contract
This example installs Tevm's bundled SimpleContract bytecode, writes through viem, mines the pending transaction, and reads the resulting state.
import { createMemoryClient, PREFUNDED_ACCOUNTS } from 'tevm'
import { SimpleContract } from 'tevm/contract'
import { expect, test } from 'vitest'
test('writes and reads contract state', async () => {
const client = createMemoryClient({
miningConfig: { type: 'manual' },
})
const contract = SimpleContract.withAddress(
'0x2222222222222222222222222222222222222222',
)
await client.setCode({
address: contract.address,
bytecode: contract.deployedBytecode,
})
const hash = await client.writeContract({
account: PREFUNDED_ACCOUNTS[0],
address: contract.address,
abi: contract.abi,
functionName: 'set',
args: [42n],
})
await client.mine({ blocks: 1 })
await client.waitForTransactionReceipt({ hash })
const value = await client.readContract({
address: contract.address,
abi: contract.abi,
functionName: 'get',
})
expect(value).toBe(42n)
})Notice the two mining APIs:
client.tevmMine({ blockCount: 1 })is the Tevm action.client.mine({ blocks: 1 })is viem's Anvil-compatible test action.
Isolate Tests with Snapshots
Use viem test actions to snapshot and restore the complete local chain.
import { createMemoryClient } from 'tevm'
import { expect, test } from 'vitest'
test('restores a snapshot', async () => {
const client = createMemoryClient()
const account = '0x3333333333333333333333333333333333333333'
const snapshotId = await client.snapshot()
await client.setBalance({ address: account, value: 100n })
expect(await client.getBalance({ address: account })).toBe(100n)
await client.revert({ id: snapshotId })
expect(await client.getBalance({ address: account })).toBe(0n)
})Create a fresh client per test when possible. Snapshots are useful when setup is expensive or several assertions need the same baseline.
Control Block Time
The viem test actions control the next block without mutating block objects directly.
import { createMemoryClient } from 'tevm'
import { expect, test } from 'vitest'
test('sets the next block timestamp', async () => {
const client = createMemoryClient()
const timestamp = 2_000_000_000n
await client.setNextBlockTimestamp({ timestamp })
await client.mine({ blocks: 1 })
const block = await client.getBlock({ blockTag: 'latest' })
expect(block.timestamp).toBe(timestamp)
})For automatic interval mining, configure seconds with miningConfig: { type: 'interval', blockTime: 2 }. Use manual mining in tests that need deterministic transaction boundaries.
Inspect Execution
tevmCall exposes opcode hooks and a Geth-style trace. This is useful for gas assertions and debugger tests.
import { createMemoryClient, PREFUNDED_ACCOUNTS } from 'tevm'
import { expect, test } from 'vitest'
test('collects an execution trace', async () => {
const client = createMemoryClient()
const contract = '0x4444444444444444444444444444444444444444'
await client.tevmSetAccount({
address: contract,
deployedBytecode: '0x6001600055',
})
const opcodes: string[] = []
const result = await client.tevmCall({
from: PREFUNDED_ACCOUNTS[0].address,
to: contract,
createTrace: true,
onStep(step, next) {
opcodes.push(step.opcode.name)
next?.()
},
})
expect(result.errors).toBeUndefined()
expect(opcodes).toEqual(['PUSH1', 'PUSH1', 'SSTORE'])
expect(result.trace?.structLogs).toHaveLength(3)
})Use the EIP-1193 Surface
The client's request method accepts the EIP-1193 shape: { method, params? }. It does not accept a JSON-RPC envelope with id or jsonrpc.
import { createMemoryClient } from 'tevm'
import { expect, test } from 'vitest'
test('serves EIP-1193 requests', async () => {
const client = createMemoryClient()
const chainId = await client.request({ method: 'eth_chainId' })
const blockNumber = await client.request({ method: 'eth_blockNumber' })
expect(chainId).toBe('0x384')
expect(blockNumber).toBe('0x0')
})
